• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
pinFragmentDone(int requestCode, boolean success)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      */
getPinDisabledUntil()122     public abstract long getPinDisabledUntil();
123 
124     /**
125      * Set the bad PIN retry time
126      * @param retryDisableTimeout Retry time
127      */
setPinDisabledUntil(long retryDisableTimeout)128     public abstract void setPinDisabledUntil(long retryDisableTimeout);
129 
130     /**
131      * Set PIN password for the profile
132      * @param pin New PIN password
133      */
setPin(String pin, String originalPin)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      */
deletePin(String oldPin)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      */
isPinCorrect(String pin)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      */
isPinSet()153     public abstract boolean isPinSet();
154 
PinDialogFragment()155     public PinDialogFragment() {
156         mRetCode = PIN_DIALOG_RESULT_FAIL;
157     }
158 
159     @Override
onCreate(Bundle savedInstanceState)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
onCreateDialog(Bundle savedInstanceState)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 
updateWrongPin()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