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