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