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