1 /* 2 * Copyright (C) 2014 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.tv.settings.dialog; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorInflater; 21 import android.app.Dialog; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.res.Resources; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.text.Editable; 28 import android.text.TextUtils; 29 import android.text.TextWatcher; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.util.TypedValue; 33 import android.view.KeyEvent; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.accessibility.AccessibilityManager; 38 import android.widget.EditText; 39 import android.widget.FrameLayout; 40 import android.widget.OverScroller; 41 import android.widget.TextView; 42 import android.widget.Toast; 43 44 import androidx.annotation.IntDef; 45 import androidx.fragment.app.Fragment; 46 47 import com.android.tv.settings.R; 48 49 import java.lang.annotation.Retention; 50 import java.lang.annotation.RetentionPolicy; 51 import java.util.function.Consumer; 52 53 public abstract class PinDialogFragment extends SafeDismissDialogFragment 54 implements DialogInterface.OnDismissListener { 55 private static final String TAG = PinDialogFragment.class.getSimpleName(); 56 private static final boolean DEBUG = false; 57 58 protected static final String ARG_TYPE = "type"; 59 60 @Retention(RetentionPolicy.SOURCE) 61 @IntDef({PIN_DIALOG_TYPE_UNLOCK_CHANNEL, 62 PIN_DIALOG_TYPE_UNLOCK_PROGRAM, 63 PIN_DIALOG_TYPE_ENTER_PIN, 64 PIN_DIALOG_TYPE_NEW_PIN, 65 PIN_DIALOG_TYPE_OLD_PIN, 66 PIN_DIALOG_TYPE_DELETE_PIN}) 67 public @interface PinDialogType {} 68 /** 69 * PIN code dialog for unlock channel 70 */ 71 public static final int PIN_DIALOG_TYPE_UNLOCK_CHANNEL = 0; 72 73 /** 74 * PIN code dialog for unlock content. 75 * Only difference between {@code PIN_DIALOG_TYPE_UNLOCK_CHANNEL} is it's title. 76 */ 77 public static final int PIN_DIALOG_TYPE_UNLOCK_PROGRAM = 1; 78 79 /** 80 * PIN code dialog for change parental control settings 81 */ 82 public static final int PIN_DIALOG_TYPE_ENTER_PIN = 2; 83 84 /** 85 * PIN code dialog for set new PIN 86 */ 87 public static final int PIN_DIALOG_TYPE_NEW_PIN = 3; 88 89 // PIN code dialog for checking old PIN. This is intenal only. 90 private static final int PIN_DIALOG_TYPE_OLD_PIN = 4; 91 92 /** 93 * PIN code dialog for deleting the PIN 94 */ 95 public static final int PIN_DIALOG_TYPE_DELETE_PIN = 5; 96 97 private static final int PIN_DIALOG_RESULT_SUCCESS = 0; 98 private static final int PIN_DIALOG_RESULT_FAIL = 1; 99 100 private static final int MAX_WRONG_PIN_COUNT = 5; 101 private static final int WRONG_PIN_REFRESH_DELAY = 1000; 102 private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute 103 104 public interface ResultListener { pinFragmentDone(int requestCode, boolean success)105 void pinFragmentDone(int requestCode, boolean success); 106 } 107 108 public static final String DIALOG_TAG = PinDialogFragment.class.getName(); 109 110 private static final int NUMBER_PICKERS_RES_ID[] = { 111 R.id.first, R.id.second, R.id.third, R.id.fourth }; 112 113 private int mType; 114 private int mRetCode; 115 116 private TextView mWrongPinView; 117 private View mEnterPinView; 118 private TextView mTitleView; 119 private PinNumberPicker[] mPickers; 120 private EditText mAccessiblePin; 121 private String mOriginalPin; 122 private String mPrevPin; 123 private int mWrongPinCount; 124 private long mDisablePinUntil; 125 private boolean mIsPinSet; 126 private boolean mIsDispatched; 127 128 private final Handler mHandler = new Handler(); 129 130 /** 131 * Get the bad PIN retry time 132 * @return Retry time 133 */ getPinDisabledUntil()134 public abstract long getPinDisabledUntil(); 135 136 /** 137 * Set the bad PIN retry time 138 * @param retryDisableTimeout Retry time 139 */ setPinDisabledUntil(long retryDisableTimeout)140 public abstract void setPinDisabledUntil(long retryDisableTimeout); 141 142 /** 143 * Set PIN password for the profile 144 * @param pin New PIN password 145 * @param consumer Will be called with the success result from setting the pin 146 */ setPin(String pin, String originalPin, Consumer<Boolean> consumer)147 public abstract void setPin(String pin, String originalPin, Consumer<Boolean> consumer); 148 149 /** 150 * Delete PIN password for the profile 151 * @param oldPin Old PIN password (required) 152 * @param consumer Will be called with the success result from deleting the pin 153 */ deletePin(String oldPin, Consumer<Boolean> consumer)154 public abstract void deletePin(String oldPin, Consumer<Boolean> consumer); 155 156 /** 157 * Validate PIN password for the profile 158 * @param pin Password to check 159 * @param consumer Will be called with the result of the check 160 */ isPinCorrect(String pin, Consumer<Boolean> consumer)161 public abstract void isPinCorrect(String pin, Consumer<Boolean> consumer); 162 163 /** 164 * Check if there is a PIN password set on the profile 165 * @param consumer Will be called with the result of the check 166 */ isPinSet(Consumer<Boolean> consumer)167 public abstract void isPinSet(Consumer<Boolean> consumer); 168 PinDialogFragment()169 public PinDialogFragment() { 170 mRetCode = PIN_DIALOG_RESULT_FAIL; 171 } 172 173 @Override onCreate(Bundle savedInstanceState)174 public void onCreate(Bundle savedInstanceState) { 175 super.onCreate(savedInstanceState); 176 setStyle(STYLE_NO_TITLE, 0); 177 mDisablePinUntil = getPinDisabledUntil(); 178 final Bundle args = getArguments(); 179 if (!args.containsKey(ARG_TYPE)) { 180 throw new IllegalStateException("Fragment arguments must specify type"); 181 } 182 mType = getArguments().getInt(ARG_TYPE); 183 } 184 185 @Override onCreateDialog(Bundle savedInstanceState)186 public Dialog onCreateDialog(Bundle savedInstanceState) { 187 Dialog dlg = super.onCreateDialog(savedInstanceState); 188 dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation; 189 PinNumberPicker.loadResources(dlg.getContext()); 190 return dlg; 191 } 192 193 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)194 public View onCreateView(LayoutInflater inflater, ViewGroup container, 195 Bundle savedInstanceState) { 196 final View v = inflater.inflate(R.layout.pin_dialog, container, false); 197 198 mWrongPinView = v.findViewById(R.id.wrong_pin); 199 mEnterPinView = v.findViewById(R.id.enter_pin); 200 if (mEnterPinView == null) { 201 throw new IllegalStateException("R.id.enter_pin missing!"); 202 } 203 mTitleView = mEnterPinView.findViewById(R.id.title); 204 isPinSet(result -> dispatchOnIsPinSet(result, savedInstanceState, v)); 205 206 return v; 207 } 208 209 @Override onDismiss(DialogInterface dialog)210 public void onDismiss(DialogInterface dialog) { 211 super.onDismiss(dialog); 212 if (DEBUG) Log.d(TAG, "onDismiss: mRetCode=" + mRetCode); 213 214 boolean result = mRetCode == PIN_DIALOG_RESULT_SUCCESS; 215 Fragment f = getTargetFragment(); 216 if (f instanceof ResultListener) { 217 ((ResultListener) f).pinFragmentDone(getTargetRequestCode(), result); 218 } else if (getActivity() instanceof ResultListener) { 219 final ResultListener listener = (ResultListener) getActivity(); 220 listener.pinFragmentDone(getTargetRequestCode(), result); 221 } 222 } 223 updateWrongPin()224 private void updateWrongPin() { 225 if (getActivity() == null) { 226 // The activity is already detached. No need to update. 227 mHandler.removeCallbacks(null); 228 return; 229 } 230 231 final long secondsLeft = (mDisablePinUntil - System.currentTimeMillis()) / 1000; 232 final boolean enabled = secondsLeft < 1; 233 if (enabled) { 234 mWrongPinView.setVisibility(View.GONE); 235 mEnterPinView.setVisibility(View.VISIBLE); 236 mWrongPinCount = 0; 237 } else { 238 mEnterPinView.setVisibility(View.GONE); 239 mWrongPinView.setVisibility(View.VISIBLE); 240 mWrongPinView.setText(getResources().getString(R.string.pin_enter_wrong_seconds, 241 secondsLeft)); 242 mHandler.postDelayed(this::updateWrongPin, WRONG_PIN_REFRESH_DELAY); 243 } 244 } 245 246 private void exit(int retCode) { 247 mRetCode = retCode; 248 mIsDispatched = false; 249 dismiss(); 250 } 251 252 private void handleWrongPin() { 253 if (++mWrongPinCount >= MAX_WRONG_PIN_COUNT) { 254 mDisablePinUntil = System.currentTimeMillis() + DISABLE_PIN_DURATION_MILLIS; 255 setPinDisabledUntil(mDisablePinUntil); 256 updateWrongPin(); 257 } else { 258 showToast(R.string.pin_toast_wrong); 259 } 260 } 261 262 private void showToast(int resId) { 263 Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show(); 264 } 265 266 private void done(String pin) { 267 if (DEBUG) Log.d(TAG, "done: mType=" + mType + " pin=" + pin); 268 if (mIsDispatched) { 269 // avoid re-triggering any of the dispatch methods if the user 270 // double clicks in the pin dialog 271 return; 272 } 273 switch (mType) { 274 case PIN_DIALOG_TYPE_UNLOCK_CHANNEL: 275 case PIN_DIALOG_TYPE_UNLOCK_PROGRAM: 276 case PIN_DIALOG_TYPE_ENTER_PIN: 277 dispatchOnPinEntered(pin); 278 break; 279 case PIN_DIALOG_TYPE_DELETE_PIN: 280 dispatchOnDeletePin(pin); 281 break; 282 case PIN_DIALOG_TYPE_NEW_PIN: 283 dispatchOnNewPinTyped(pin); 284 break; 285 case PIN_DIALOG_TYPE_OLD_PIN: 286 dispatchOnOldPinTyped(pin); 287 break; 288 } 289 } 290 291 private void dispatchOnPinEntered(String pin) { 292 isPinCorrect(pin, pinCorrect -> { 293 if (!mIsPinSet || pinCorrect) { 294 exit(PIN_DIALOG_RESULT_SUCCESS); 295 } else { 296 resetPinInput(); 297 handleWrongPin(); 298 } 299 }); 300 } 301 302 private void dispatchOnDeletePin(String pin) { 303 isPinCorrect(pin, pinIsCorrect -> { 304 if (pinIsCorrect) { 305 mIsDispatched = true; 306 deletePin(pin, success -> { 307 exit(success ? PIN_DIALOG_RESULT_SUCCESS : PIN_DIALOG_RESULT_FAIL); 308 }); 309 } else { 310 resetPinInput(); 311 handleWrongPin(); 312 } 313 }); 314 } 315 316 private void dispatchOnNewPinTyped(String pin) { 317 if (mPrevPin == null) { 318 resetPinInput(); 319 mPrevPin = pin; 320 mTitleView.setText(R.string.pin_enter_again); 321 } else { 322 if (pin.equals(mPrevPin)) { 323 mIsDispatched = true; 324 setPin(pin, mOriginalPin, success -> { 325 exit(PIN_DIALOG_RESULT_SUCCESS); 326 }); 327 } else { 328 resetPinInput(); 329 mTitleView.setText(R.string.pin_enter_new_pin); 330 mPrevPin = null; 331 showToast(R.string.pin_toast_not_match); 332 } 333 } 334 } 335 336 private void dispatchOnOldPinTyped(String pin) { 337 resetPinInput(); 338 isPinCorrect(pin, pinIsCorrect -> { 339 if (isAdded()) { 340 if (pinIsCorrect) { 341 mOriginalPin = pin; 342 mType = PIN_DIALOG_TYPE_NEW_PIN; 343 mTitleView.setText(R.string.pin_enter_new_pin); 344 } else { 345 handleWrongPin(); 346 } 347 } 348 }); 349 } 350 351 public int getType() { 352 return mType; 353 } 354 355 private void dispatchOnIsPinSet(Boolean result, Bundle savedInstanceState, View v) { 356 mIsPinSet = result; 357 if (!mIsPinSet) { 358 // If PIN isn't set, user should set a PIN. 359 // Successfully setting a new set is considered as entering correct PIN. 360 mType = PIN_DIALOG_TYPE_NEW_PIN; 361 } 362 363 mEnterPinView.setVisibility(View.VISIBLE); 364 setDialogTitle(); 365 setUpPinNumberPicker(savedInstanceState, v); 366 } 367 368 private void setDialogTitle() { 369 switch (mType) { 370 case PIN_DIALOG_TYPE_UNLOCK_CHANNEL: 371 mTitleView.setText(R.string.pin_enter_unlock_channel); 372 break; 373 case PIN_DIALOG_TYPE_UNLOCK_PROGRAM: 374 mTitleView.setText(R.string.pin_enter_unlock_program); 375 break; 376 case PIN_DIALOG_TYPE_ENTER_PIN: 377 case PIN_DIALOG_TYPE_DELETE_PIN: 378 mTitleView.setText(R.string.pin_enter_pin); 379 break; 380 case PIN_DIALOG_TYPE_NEW_PIN: 381 if (!mIsPinSet) { 382 mTitleView.setText(R.string.pin_enter_new_pin); 383 } else { 384 mTitleView.setText(R.string.pin_enter_old_pin); 385 mType = PIN_DIALOG_TYPE_OLD_PIN; 386 } 387 } 388 } 389 390 private void setUpPinNumberPicker(Bundle savedInstanceState, View v) { 391 if (mType != PIN_DIALOG_TYPE_NEW_PIN) { 392 updateWrongPin(); 393 } 394 395 AccessibilityManager accessibilityManager = getContext().getSystemService( 396 AccessibilityManager.class); 397 // Use built in EditText in accessibility mode as it can use custom input methods 398 // and other needed behaviors. 399 boolean isAccessible = accessibilityManager.isEnabled() && 400 accessibilityManager.isTouchExplorationEnabled(); 401 402 mAccessiblePin = v.requireViewById(R.id.accessible_pin); 403 mAccessiblePin.setVisibility(isAccessible ? View.VISIBLE : View.GONE); 404 mAccessiblePin.addTextChangedListener(new TextWatcher() { 405 @Override 406 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 407 } 408 409 @Override 410 public void onTextChanged(CharSequence s, int start, int before, int count) { 411 if (s.length() == 4) { 412 done(s.toString()); 413 } 414 } 415 416 @Override 417 public void afterTextChanged(Editable s) { 418 } 419 }); 420 421 mPickers = new PinNumberPicker[NUMBER_PICKERS_RES_ID.length]; 422 for (int i = 0; i < NUMBER_PICKERS_RES_ID.length; i++) { 423 mPickers[i] = v.findViewById(NUMBER_PICKERS_RES_ID[i]); 424 mPickers[i].setValueRange(0, 9); 425 mPickers[i].setPinDialogFragment(this); 426 mPickers[i].updateFocus(); 427 mPickers[i].setVisibility(isAccessible ? View.GONE : View.VISIBLE); 428 } 429 for (int i = 0; i < NUMBER_PICKERS_RES_ID.length - 1; i++) { 430 mPickers[i].setNextNumberPicker(mPickers[i + 1]); 431 } 432 433 if (savedInstanceState == null) { 434 (isAccessible ? mAccessiblePin : mPickers[0]).requestFocus(); 435 } 436 } 437 438 private String getPinInput() { 439 String result = ""; 440 try { 441 for (PinNumberPicker pnp : mPickers) { 442 result += pnp.getValue(); 443 } 444 } catch (IllegalStateException e) { 445 result = ""; 446 } 447 return result; 448 } 449 450 private void resetPinInput() { 451 for (PinNumberPicker pnp : mPickers) { 452 pnp.setValueRange(0, 9); 453 } 454 (mAccessiblePin.getVisibility() == View.VISIBLE ? mAccessiblePin : mPickers[0]) 455 .requestFocus(); 456 mAccessiblePin.setText(""); 457 } 458 459 public static final class PinNumberPicker extends FrameLayout { 460 private static final int NUMBER_VIEWS_RES_ID[] = { 461 R.id.previous2_number, 462 R.id.previous_number, 463 R.id.current_number, 464 R.id.next_number, 465 R.id.next2_number }; 466 private static final int CURRENT_NUMBER_VIEW_INDEX = 2; 467 468 private static Animator sFocusedNumberEnterAnimator; 469 private static Animator sFocusedNumberExitAnimator; 470 private static Animator sAdjacentNumberEnterAnimator; 471 private static Animator sAdjacentNumberExitAnimator; 472 473 private static float sAlphaForFocusedNumber; 474 private static float sAlphaForAdjacentNumber; 475 476 private int mMinValue; 477 private int mMaxValue; 478 private int mCurrentValue; 479 private int mNextValue; 480 private final int mNumberViewHeight; 481 private PinDialogFragment mDialog; 482 private PinNumberPicker mNextNumberPicker; 483 private boolean mCancelAnimation; 484 485 private final View mNumberViewHolder; 486 private final View mBackgroundView; 487 private final TextView[] mNumberViews; 488 private final OverScroller mScroller; 489 490 public PinNumberPicker(Context context) { 491 this(context, null); 492 } 493 494 public PinNumberPicker(Context context, AttributeSet attrs) { 495 this(context, attrs, 0); 496 } 497 498 public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) { 499 this(context, attrs, defStyleAttr, 0); 500 } 501 502 public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr, 503 int defStyleRes) { 504 super(context, attrs, defStyleAttr, defStyleRes); 505 View view = inflate(context, R.layout.pin_number_picker, this); 506 mNumberViewHolder = view.findViewById(R.id.number_view_holder); 507 if (mNumberViewHolder == null) { 508 throw new IllegalStateException("R.id.number_view_holder missing!"); 509 } 510 mBackgroundView = view.findViewById(R.id.focused_background); 511 mNumberViews = new TextView[NUMBER_VIEWS_RES_ID.length]; 512 for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { 513 mNumberViews[i] = view.findViewById(NUMBER_VIEWS_RES_ID[i]); 514 } 515 Resources resources = context.getResources(); 516 mNumberViewHeight = resources.getDimensionPixelOffset( 517 R.dimen.pin_number_picker_text_view_height); 518 519 mScroller = new OverScroller(context); 520 521 mNumberViewHolder.setOnFocusChangeListener((v, hasFocus) -> updateFocus()); 522 523 mNumberViewHolder.setOnKeyListener((v, keyCode, event) -> { 524 if (event.getAction() == KeyEvent.ACTION_DOWN) { 525 switch (keyCode) { 526 case KeyEvent.KEYCODE_DPAD_UP: 527 case KeyEvent.KEYCODE_DPAD_DOWN: { 528 if (!mScroller.isFinished() || mCancelAnimation) { 529 endScrollAnimation(); 530 } 531 if (mScroller.isFinished() || mCancelAnimation) { 532 mCancelAnimation = false; 533 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 534 mNextValue = adjustValueInValidRange(mCurrentValue + 1); 535 startScrollAnimation(true); 536 mScroller.startScroll(0, 0, 0, mNumberViewHeight, 537 getResources().getInteger( 538 R.integer.pin_number_scroll_duration)); 539 } else { 540 mNextValue = adjustValueInValidRange(mCurrentValue - 1); 541 startScrollAnimation(false); 542 mScroller.startScroll(0, 0, 0, -mNumberViewHeight, 543 getResources().getInteger( 544 R.integer.pin_number_scroll_duration)); 545 } 546 updateText(); 547 invalidate(); 548 } 549 return true; 550 } 551 } 552 } else if (event.getAction() == KeyEvent.ACTION_UP) { 553 switch (keyCode) { 554 case KeyEvent.KEYCODE_DPAD_UP: 555 case KeyEvent.KEYCODE_DPAD_DOWN: { 556 mCancelAnimation = true; 557 return true; 558 } 559 } 560 } 561 return false; 562 }); 563 mNumberViewHolder.setScrollY(mNumberViewHeight); 564 } 565 566 static void loadResources(Context context) { 567 if (sFocusedNumberEnterAnimator == null) { 568 TypedValue outValue = new TypedValue(); 569 context.getResources().getValue( 570 R.dimen.pin_alpha_for_focused_number, outValue, true); 571 sAlphaForFocusedNumber = outValue.getFloat(); 572 context.getResources().getValue( 573 R.dimen.pin_alpha_for_adjacent_number, outValue, true); 574 sAlphaForAdjacentNumber = outValue.getFloat(); 575 576 sFocusedNumberEnterAnimator = AnimatorInflater.loadAnimator(context, 577 R.animator.pin_focused_number_enter); 578 sFocusedNumberExitAnimator = AnimatorInflater.loadAnimator(context, 579 R.animator.pin_focused_number_exit); 580 sAdjacentNumberEnterAnimator = AnimatorInflater.loadAnimator(context, 581 R.animator.pin_adjacent_number_enter); 582 sAdjacentNumberExitAnimator = AnimatorInflater.loadAnimator(context, 583 R.animator.pin_adjacent_number_exit); 584 } 585 } 586 587 @Override 588 public void computeScroll() { 589 super.computeScroll(); 590 if (mScroller.computeScrollOffset()) { 591 mNumberViewHolder.setScrollY(mScroller.getCurrY() + mNumberViewHeight); 592 updateText(); 593 invalidate(); 594 } else if (mCurrentValue != mNextValue) { 595 mCurrentValue = mNextValue; 596 } 597 } 598 599 @Override 600 public boolean dispatchKeyEvent(KeyEvent event) { 601 if (event.getAction() == KeyEvent.ACTION_UP) { 602 int keyCode = event.getKeyCode(); 603 if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { 604 jumpNextValue(keyCode - KeyEvent.KEYCODE_0); 605 } else if (keyCode != KeyEvent.KEYCODE_DPAD_CENTER 606 && keyCode != KeyEvent.KEYCODE_ENTER) { 607 return super.dispatchKeyEvent(event); 608 } 609 if (mNextNumberPicker == null) { 610 String pin = mDialog.getPinInput(); 611 if (!TextUtils.isEmpty(pin)) { 612 mDialog.done(pin); 613 } 614 } else { 615 mNextNumberPicker.requestFocus(); 616 } 617 return true; 618 } 619 return super.dispatchKeyEvent(event); 620 } 621 622 @Override 623 public void setEnabled(boolean enabled) { 624 super.setEnabled(enabled); 625 mNumberViewHolder.setFocusable(enabled); 626 for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { 627 mNumberViews[i].setEnabled(enabled); 628 } 629 } 630 631 void startScrollAnimation(boolean scrollUp) { 632 if (scrollUp) { 633 sAdjacentNumberExitAnimator.setTarget(mNumberViews[1]); 634 sFocusedNumberExitAnimator.setTarget(mNumberViews[2]); 635 sFocusedNumberEnterAnimator.setTarget(mNumberViews[3]); 636 sAdjacentNumberEnterAnimator.setTarget(mNumberViews[4]); 637 } else { 638 sAdjacentNumberEnterAnimator.setTarget(mNumberViews[0]); 639 sFocusedNumberEnterAnimator.setTarget(mNumberViews[1]); 640 sFocusedNumberExitAnimator.setTarget(mNumberViews[2]); 641 sAdjacentNumberExitAnimator.setTarget(mNumberViews[3]); 642 } 643 sAdjacentNumberExitAnimator.start(); 644 sFocusedNumberExitAnimator.start(); 645 sFocusedNumberEnterAnimator.start(); 646 sAdjacentNumberEnterAnimator.start(); 647 } 648 649 void endScrollAnimation() { 650 sAdjacentNumberExitAnimator.end(); 651 sFocusedNumberExitAnimator.end(); 652 sFocusedNumberEnterAnimator.end(); 653 sAdjacentNumberEnterAnimator.end(); 654 mCurrentValue = mNextValue; 655 mNumberViews[1].setAlpha(sAlphaForAdjacentNumber); 656 mNumberViews[2].setAlpha(sAlphaForFocusedNumber); 657 mNumberViews[3].setAlpha(sAlphaForAdjacentNumber); 658 } 659 660 void setValueRange(int min, int max) { 661 if (min > max) { 662 throw new IllegalArgumentException( 663 "The min value should be greater than or equal to the max value"); 664 } 665 mMinValue = min; 666 mMaxValue = max; 667 mNextValue = mCurrentValue = mMinValue - 1; 668 clearText(); 669 mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText("—"); 670 } 671 672 void setPinDialogFragment(PinDialogFragment dlg) { 673 mDialog = dlg; 674 } 675 676 void setNextNumberPicker(PinNumberPicker picker) { 677 mNextNumberPicker = picker; 678 } 679 680 int getValue() { 681 if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) { 682 throw new IllegalStateException("Value is not set"); 683 } 684 return mCurrentValue; 685 } 686 687 void jumpNextValue(int value) { 688 if (value < mMinValue || value > mMaxValue) { 689 throw new IllegalStateException("Value is not set"); 690 } 691 mNextValue = mCurrentValue = adjustValueInValidRange(value); 692 updateText(); 693 } 694 695 void updateFocus() { 696 endScrollAnimation(); 697 if (mNumberViewHolder.isFocused()) { 698 mBackgroundView.setVisibility(View.VISIBLE); 699 updateText(); 700 } else { 701 mBackgroundView.setVisibility(View.GONE); 702 if (!mScroller.isFinished()) { 703 mCurrentValue = mNextValue; 704 mScroller.abortAnimation(); 705 } 706 clearText(); 707 mNumberViewHolder.setScrollY(mNumberViewHeight); 708 } 709 } 710 711 private void clearText() { 712 for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { 713 if (i != CURRENT_NUMBER_VIEW_INDEX) { 714 mNumberViews[i].setText(""); 715 } else if (mCurrentValue >= mMinValue && mCurrentValue <= mMaxValue) { 716 // Bullet 717 mNumberViews[i].setText("\u2022"); 718 } 719 } 720 } 721 722 private void updateText() { 723 if (mNumberViewHolder.isFocused()) { 724 if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) { 725 mNextValue = mCurrentValue = mMinValue; 726 } 727 int value = adjustValueInValidRange(mCurrentValue - CURRENT_NUMBER_VIEW_INDEX); 728 for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { 729 mNumberViews[i].setText(String.valueOf(adjustValueInValidRange(value))); 730 value = adjustValueInValidRange(value + 1); 731 } 732 } 733 } 734 735 private int adjustValueInValidRange(int value) { 736 int interval = mMaxValue - mMinValue + 1; 737 if (value < mMinValue - interval || value > mMaxValue + interval) { 738 throw new IllegalArgumentException("The value( " + value 739 + ") is too small or too big to adjust"); 740 } 741 return (value < mMinValue) ? value + interval 742 : (value > mMaxValue) ? value - interval : value; 743 } 744 } 745 } 746