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