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