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