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