1/*
2 * Copyright (C) 2015 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
17package com.android.tv.dialog;
18
19import android.animation.Animator;
20import android.animation.AnimatorInflater;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.animation.ValueAnimator;
25import android.app.ActivityManager;
26import android.app.Dialog;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.SharedPreferences;
30import android.content.res.Resources;
31import android.os.Bundle;
32import android.os.Handler;
33import android.preference.PreferenceManager;
34import android.text.TextUtils;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.util.TypedValue;
38import android.view.KeyEvent;
39import android.view.LayoutInflater;
40import android.view.View;
41import android.view.ViewGroup;
42import android.view.ViewGroup.LayoutParams;
43import android.widget.FrameLayout;
44import android.widget.TextView;
45import android.widget.Toast;
46
47import com.android.tv.R;
48import com.android.tv.util.TvSettings;
49
50public class PinDialogFragment extends SafeDismissDialogFragment {
51    private static final String TAG = "PinDialogFragment";
52    private static final boolean DBG = true;
53
54    /**
55     * PIN code dialog for unlock channel
56     */
57    public static final int PIN_DIALOG_TYPE_UNLOCK_CHANNEL = 0;
58
59    /**
60     * PIN code dialog for unlock content.
61     * Only difference between {@code PIN_DIALOG_TYPE_UNLOCK_CHANNEL} is it's title.
62     */
63    public static final int PIN_DIALOG_TYPE_UNLOCK_PROGRAM = 1;
64
65    /**
66     * PIN code dialog for change parental control settings
67     */
68    public static final int PIN_DIALOG_TYPE_ENTER_PIN = 2;
69
70    /**
71     * PIN code dialog for set new PIN
72     */
73    public static final int PIN_DIALOG_TYPE_NEW_PIN = 3;
74
75    // PIN code dialog for checking old PIN. This is internal only.
76    private static final int PIN_DIALOG_TYPE_OLD_PIN = 4;
77
78    private static final int PIN_DIALOG_RESULT_SUCCESS = 0;
79    private static final int PIN_DIALOG_RESULT_FAIL = 1;
80
81    private static final int MAX_WRONG_PIN_COUNT = 5;
82    private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute
83
84    private static final String INITIAL_TEXT = "—";
85    private static final String TRACKER_LABEL = "Pin dialog";
86
87    public interface ResultListener {
88        void done(boolean success);
89    }
90
91    public static final String DIALOG_TAG = PinDialogFragment.class.getName();
92
93    private static final int NUMBER_PICKERS_RES_ID[] = {
94            R.id.first, R.id.second, R.id.third, R.id.fourth };
95
96    private int mType;
97    private ResultListener mListener;
98    private int mRetCode;
99
100    private TextView mWrongPinView;
101    private View mEnterPinView;
102    private TextView mTitleView;
103    private PinNumberPicker[] mPickers;
104    private SharedPreferences mSharedPreferences;
105    private String mPrevPin;
106    private String mPin;
107    private int mWrongPinCount;
108    private long mDisablePinUntil;
109    private final Handler mHandler = new Handler();
110
111    public PinDialogFragment(int type, ResultListener listener) {
112        mType = type;
113        mListener = listener;
114        mRetCode = PIN_DIALOG_RESULT_FAIL;
115    }
116
117    @Override
118    public void onCreate(Bundle savedInstanceState) {
119        super.onCreate(savedInstanceState);
120        setStyle(STYLE_NO_TITLE, 0);
121        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
122        mDisablePinUntil = TvSettings.getDisablePinUntil(getActivity());
123        if (ActivityManager.isUserAMonkey()) {
124            // Skip PIN dialog half the time for monkeys
125            if (Math.random() < 0.5) {
126                exit(PIN_DIALOG_RESULT_SUCCESS);
127            }
128        }
129    }
130
131    @Override
132    public Dialog onCreateDialog(Bundle savedInstanceState) {
133        Dialog dlg = super.onCreateDialog(savedInstanceState);
134        dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation;
135        PinNumberPicker.loadResources(dlg.getContext());
136        return dlg;
137    }
138
139    @Override
140    public String getTrackerLabel() {
141        return TRACKER_LABEL;
142    }
143
144    @Override
145    public void onStart() {
146        super.onStart();
147        // Dialog size is determined by its windows size, not inflated view size.
148        // So apply view size to window after the DialogFragment.onStart() where dialog is shown.
149        Dialog dlg = getDialog();
150        if (dlg != null) {
151            dlg.getWindow().setLayout(
152                    getResources().getDimensionPixelSize(R.dimen.pin_dialog_width),
153                    LayoutParams.WRAP_CONTENT);
154        }
155    }
156
157    @Override
158    public View onCreateView(LayoutInflater inflater, ViewGroup container,
159            Bundle savedInstanceState) {
160        final View v = inflater.inflate(R.layout.pin_dialog, container, false);
161
162        mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin);
163        mEnterPinView = v.findViewById(R.id.enter_pin);
164        mTitleView = (TextView) mEnterPinView.findViewById(R.id.title);
165        if (TextUtils.isEmpty(getPin())) {
166            // If PIN isn't set, user should set a PIN.
167            // Successfully setting a new set is considered as entering correct PIN.
168            mType = PIN_DIALOG_TYPE_NEW_PIN;
169        }
170        switch (mType) {
171            case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
172                mTitleView.setText(R.string.pin_enter_unlock_channel);
173                break;
174            case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
175                mTitleView.setText(R.string.pin_enter_unlock_program);
176                break;
177            case PIN_DIALOG_TYPE_ENTER_PIN:
178                mTitleView.setText(R.string.pin_enter_pin);
179                break;
180            case PIN_DIALOG_TYPE_NEW_PIN:
181                if (TextUtils.isEmpty(getPin())) {
182                    mTitleView.setText(R.string.pin_enter_create_pin);
183                } else {
184                    mTitleView.setText(R.string.pin_enter_old_pin);
185                    mType = PIN_DIALOG_TYPE_OLD_PIN;
186                }
187        }
188
189        mPickers = new PinNumberPicker[NUMBER_PICKERS_RES_ID.length];
190        for (int i = 0; i < NUMBER_PICKERS_RES_ID.length; i++) {
191            mPickers[i] = (PinNumberPicker) v.findViewById(NUMBER_PICKERS_RES_ID[i]);
192            mPickers[i].setValueRangeAndResetText(0, 9);
193            mPickers[i].setPinDialogFragment(this);
194            mPickers[i].updateFocus(false);
195        }
196        for (int i = 0; i < NUMBER_PICKERS_RES_ID.length - 1; i++) {
197            mPickers[i].setNextNumberPicker(mPickers[i + 1]);
198        }
199
200        if (mType != PIN_DIALOG_TYPE_NEW_PIN) {
201            updateWrongPin();
202        }
203        return v;
204    }
205
206    public void setResultListener(ResultListener listener) {
207        mListener = listener;
208    }
209
210    private final Runnable mUpdateEnterPinRunnable = new Runnable() {
211        @Override
212        public void run() {
213            updateWrongPin();
214        }
215    };
216
217    private void updateWrongPin() {
218        if (getActivity() == null) {
219            // The activity is already detached. No need to update.
220            mHandler.removeCallbacks(null);
221            return;
222        }
223
224        int remainingSeconds = (int) ((mDisablePinUntil - System.currentTimeMillis()) / 1000);
225        boolean enabled = remainingSeconds < 1;
226        if (enabled) {
227            mWrongPinView.setVisibility(View.INVISIBLE);
228            mEnterPinView.setVisibility(View.VISIBLE);
229            mWrongPinCount = 0;
230        } else {
231            mEnterPinView.setVisibility(View.INVISIBLE);
232            mWrongPinView.setVisibility(View.VISIBLE);
233            mWrongPinView.setText(getResources().getQuantityString(R.plurals.pin_enter_countdown,
234                    remainingSeconds, remainingSeconds));
235            mHandler.postDelayed(mUpdateEnterPinRunnable, 1000);
236        }
237    }
238
239    private void exit(int retCode) {
240        mRetCode = retCode;
241        dismiss();
242    }
243
244    @Override
245    public void onDismiss(DialogInterface dialog) {
246        super.onDismiss(dialog);
247        if (DBG) Log.d(TAG, "onDismiss: mRetCode=" + mRetCode);
248        if (mListener != null) {
249            mListener.done(mRetCode == PIN_DIALOG_RESULT_SUCCESS);
250        }
251    }
252
253    private void handleWrongPin() {
254        if (++mWrongPinCount >= MAX_WRONG_PIN_COUNT) {
255            mDisablePinUntil = System.currentTimeMillis() + DISABLE_PIN_DURATION_MILLIS;
256            TvSettings.setDisablePinUntil(getActivity(), mDisablePinUntil);
257            updateWrongPin();
258        } else {
259            showToast(R.string.pin_toast_wrong);
260        }
261    }
262
263    private void showToast(int resId) {
264        Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show();
265    }
266
267    private void done(String pin) {
268        if (DBG) Log.d(TAG, "done: mType=" + mType + " pin=" + pin + " stored=" + getPin());
269        switch (mType) {
270            case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
271            case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
272            case PIN_DIALOG_TYPE_ENTER_PIN:
273                // TODO: Implement limited number of retrials and timeout logic.
274                if (TextUtils.isEmpty(getPin()) || pin.equals(getPin())) {
275                    exit(PIN_DIALOG_RESULT_SUCCESS);
276                } else {
277                    resetPinInput();
278                    handleWrongPin();
279                }
280                break;
281            case PIN_DIALOG_TYPE_NEW_PIN:
282                resetPinInput();
283                if (mPrevPin == null) {
284                    mPrevPin = pin;
285                    mTitleView.setText(R.string.pin_enter_again);
286                } else {
287                    if (pin.equals(mPrevPin)) {
288                        setPin(pin);
289                        exit(PIN_DIALOG_RESULT_SUCCESS);
290                    } else {
291                        if (TextUtils.isEmpty(getPin())) {
292                            mTitleView.setText(R.string.pin_enter_create_pin);
293                        } else {
294                            mTitleView.setText(R.string.pin_enter_new_pin);
295                        }
296                        mPrevPin = null;
297                        showToast(R.string.pin_toast_not_match);
298                    }
299                }
300                break;
301            case PIN_DIALOG_TYPE_OLD_PIN:
302                // Call resetPinInput() here because we'll get additional PIN input
303                // regardless of the result.
304                resetPinInput();
305                if (pin.equals(getPin())) {
306                    mType = PIN_DIALOG_TYPE_NEW_PIN;
307                    mTitleView.setText(R.string.pin_enter_new_pin);
308                } else {
309                    handleWrongPin();
310                }
311                break;
312        }
313    }
314
315    public int getType() {
316        return mType;
317    }
318
319    private void setPin(String pin) {
320        if (DBG) Log.d(TAG, "setPin: " + pin);
321        mPin = pin;
322        mSharedPreferences.edit().putString(TvSettings.PREF_PIN, pin).apply();
323    }
324
325    private String getPin() {
326        if (mPin == null) {
327            mPin = mSharedPreferences.getString(TvSettings.PREF_PIN, "");
328        }
329        return mPin;
330    }
331
332    private String getPinInput() {
333        String result = "";
334        try {
335            for (PinNumberPicker pnp : mPickers) {
336                pnp.updateText();
337                result += pnp.getValue();
338            }
339        } catch (IllegalStateException e) {
340            result = "";
341        }
342        return result;
343    }
344
345    private void resetPinInput() {
346        for (PinNumberPicker pnp : mPickers) {
347            pnp.setValueRangeAndResetText(0, 9);
348        }
349        mPickers[0].requestFocus();
350    }
351
352    public static class PinNumberPicker extends FrameLayout {
353        private static final int NUMBER_VIEWS_RES_ID[] = {
354            R.id.previous2_number,
355            R.id.previous_number,
356            R.id.current_number,
357            R.id.next_number,
358            R.id.next2_number };
359        private static final int CURRENT_NUMBER_VIEW_INDEX = 2;
360        private static final int NOT_INITIALIZED = Integer.MIN_VALUE;
361
362        private static Animator sFocusedNumberEnterAnimator;
363        private static Animator sFocusedNumberExitAnimator;
364        private static Animator sAdjacentNumberEnterAnimator;
365        private static Animator sAdjacentNumberExitAnimator;
366
367        private static float sAlphaForFocusedNumber;
368        private static float sAlphaForAdjacentNumber;
369
370        private int mMinValue;
371        private int mMaxValue;
372        private int mCurrentValue;
373        // a value for setting mCurrentValue at the end of scroll animation.
374        private int mNextValue;
375        private final int mNumberViewHeight;
376        private PinDialogFragment mDialog;
377        private PinNumberPicker mNextNumberPicker;
378        private boolean mCancelAnimation;
379
380        private final View mNumberViewHolder;
381        // When the PinNumberPicker has focus, mBackgroundView will show the focused background.
382        // Also, this view is used for handling the text change animation of the current number
383        // view which is required when the current number view text is changing from INITIAL_TEXT
384        // to "0".
385        private final TextView mBackgroundView;
386        private final TextView[] mNumberViews;
387        private final AnimatorSet mFocusGainAnimator;
388        private final AnimatorSet mFocusLossAnimator;
389        private final AnimatorSet mScrollAnimatorSet;
390
391        public PinNumberPicker(Context context) {
392            this(context, null);
393        }
394
395        public PinNumberPicker(Context context, AttributeSet attrs) {
396            this(context, attrs, 0);
397        }
398
399        public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
400            this(context, attrs, defStyleAttr, 0);
401        }
402
403        public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr,
404                int defStyleRes) {
405            super(context, attrs, defStyleAttr, defStyleRes);
406            View view = inflate(context, R.layout.pin_number_picker, this);
407            mNumberViewHolder = view.findViewById(R.id.number_view_holder);
408            mBackgroundView = (TextView) view.findViewById(R.id.focused_background);
409            mNumberViews = new TextView[NUMBER_VIEWS_RES_ID.length];
410            for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
411                mNumberViews[i] = (TextView) view.findViewById(NUMBER_VIEWS_RES_ID[i]);
412            }
413            Resources resources = context.getResources();
414            mNumberViewHeight = resources.getDimensionPixelSize(
415                    R.dimen.pin_number_picker_text_view_height);
416
417            mNumberViewHolder.setOnFocusChangeListener(new OnFocusChangeListener() {
418                @Override
419                public void onFocusChange(View v, boolean hasFocus) {
420                    updateFocus(true);
421                }
422            });
423
424            mNumberViewHolder.setOnKeyListener(new OnKeyListener() {
425                @Override
426                public boolean onKey(View v, int keyCode, KeyEvent event) {
427                    if (event.getAction() == KeyEvent.ACTION_DOWN) {
428                        switch (keyCode) {
429                            case KeyEvent.KEYCODE_DPAD_UP:
430                            case KeyEvent.KEYCODE_DPAD_DOWN: {
431                                if (mCancelAnimation) {
432                                    mScrollAnimatorSet.end();
433                                }
434                                if (!mScrollAnimatorSet.isRunning()) {
435                                    mCancelAnimation = false;
436                                    if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
437                                        mNextValue = adjustValueInValidRange(mCurrentValue + 1);
438                                        startScrollAnimation(true);
439                                    } else {
440                                        mNextValue = adjustValueInValidRange(mCurrentValue - 1);
441                                        startScrollAnimation(false);
442                                    }
443                                }
444                                return true;
445                            }
446                        }
447                    } else if (event.getAction() == KeyEvent.ACTION_UP) {
448                        switch (keyCode) {
449                            case KeyEvent.KEYCODE_DPAD_UP:
450                            case KeyEvent.KEYCODE_DPAD_DOWN: {
451                                mCancelAnimation = true;
452                                return true;
453                            }
454                        }
455                    }
456                    return false;
457                }
458            });
459            mNumberViewHolder.setScrollY(mNumberViewHeight);
460
461            mFocusGainAnimator = new AnimatorSet();
462            mFocusGainAnimator.playTogether(
463                    ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1],
464                            "alpha", 0f, sAlphaForAdjacentNumber),
465                    ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX],
466                            "alpha", sAlphaForFocusedNumber, 0f),
467                    ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1],
468                            "alpha", 0f, sAlphaForAdjacentNumber),
469                    ObjectAnimator.ofFloat(mBackgroundView, "alpha", 0f, 1f));
470            mFocusGainAnimator.setDuration(context.getResources().getInteger(
471                    android.R.integer.config_shortAnimTime));
472            mFocusGainAnimator.addListener(new AnimatorListenerAdapter() {
473                @Override
474                public void onAnimationEnd(Animator animator) {
475                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText(mBackgroundView.getText());
476                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(sAlphaForFocusedNumber);
477                    mBackgroundView.setText("");
478                }
479            });
480
481            mFocusLossAnimator = new AnimatorSet();
482            mFocusLossAnimator.playTogether(
483                    ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1],
484                            "alpha", sAlphaForAdjacentNumber, 0f),
485                    ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1],
486                            "alpha", sAlphaForAdjacentNumber, 0f),
487                    ObjectAnimator.ofFloat(mBackgroundView, "alpha", 1f, 0f));
488            mFocusLossAnimator.setDuration(context.getResources().getInteger(
489                    android.R.integer.config_shortAnimTime));
490
491            mScrollAnimatorSet = new AnimatorSet();
492            mScrollAnimatorSet.setDuration(context.getResources().getInteger(
493                    R.integer.pin_number_scroll_duration));
494            mScrollAnimatorSet.addListener(new AnimatorListenerAdapter() {
495                @Override
496                public void onAnimationEnd(Animator animation) {
497                    // Set mCurrent value when scroll animation is finished.
498                    mCurrentValue = mNextValue;
499                    updateText();
500                    mNumberViewHolder.setScrollY(mNumberViewHeight);
501                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(sAlphaForAdjacentNumber);
502                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(sAlphaForFocusedNumber);
503                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(sAlphaForAdjacentNumber);
504                }
505            });
506        }
507
508        static void loadResources(Context context) {
509            if (sFocusedNumberEnterAnimator == null) {
510                TypedValue outValue = new TypedValue();
511                context.getResources().getValue(
512                        R.dimen.pin_alpha_for_focused_number, outValue, true);
513                sAlphaForFocusedNumber = outValue.getFloat();
514                context.getResources().getValue(
515                        R.dimen.pin_alpha_for_adjacent_number, outValue, true);
516                sAlphaForAdjacentNumber = outValue.getFloat();
517
518                sFocusedNumberEnterAnimator = AnimatorInflater.loadAnimator(context,
519                        R.animator.pin_focused_number_enter);
520                sFocusedNumberExitAnimator = AnimatorInflater.loadAnimator(context,
521                        R.animator.pin_focused_number_exit);
522                sAdjacentNumberEnterAnimator = AnimatorInflater.loadAnimator(context,
523                        R.animator.pin_adjacent_number_enter);
524                sAdjacentNumberExitAnimator = AnimatorInflater.loadAnimator(context,
525                        R.animator.pin_adjacent_number_exit);
526            }
527        }
528
529        @Override
530        public boolean dispatchKeyEvent(KeyEvent event) {
531            if (event.getAction() == KeyEvent.ACTION_UP) {
532                int keyCode = event.getKeyCode();
533                if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
534                    mNextValue = adjustValueInValidRange(keyCode - KeyEvent.KEYCODE_0);
535                    updateFocus(false);
536                } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
537                        || keyCode == KeyEvent.KEYCODE_ENTER) {
538                    if (mNextNumberPicker == null) {
539                        String pin = mDialog.getPinInput();
540                        if (!TextUtils.isEmpty(pin)) {
541                            mDialog.done(pin);
542                        }
543                    } else {
544                        mNextNumberPicker.requestFocus();
545                    }
546                    return true;
547                }
548            }
549            return super.dispatchKeyEvent(event);
550        }
551
552        void startScrollAnimation(boolean scrollUp) {
553            mFocusGainAnimator.end();
554            mFocusLossAnimator.end();
555            final ValueAnimator scrollAnimator = ValueAnimator.ofInt(
556                    0, scrollUp ? mNumberViewHeight : -mNumberViewHeight);
557            scrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
558                @Override
559                public void onAnimationUpdate(ValueAnimator animation) {
560                    int value = (Integer) animation.getAnimatedValue();
561                    mNumberViewHolder.setScrollY(value + mNumberViewHeight);
562                }
563            });
564            scrollAnimator.setDuration(
565                    getResources().getInteger(R.integer.pin_number_scroll_duration));
566
567            if (scrollUp) {
568                sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]);
569                sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]);
570                sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]);
571                sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 2]);
572            } else {
573                sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 2]);
574                sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]);
575                sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]);
576                sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]);
577            }
578
579            mScrollAnimatorSet.playTogether(scrollAnimator,
580                    sAdjacentNumberExitAnimator, sFocusedNumberExitAnimator,
581                    sFocusedNumberEnterAnimator, sAdjacentNumberEnterAnimator);
582            mScrollAnimatorSet.start();
583        }
584
585        void setValueRangeAndResetText(int min, int max) {
586            if (min > max) {
587                throw new IllegalArgumentException(
588                        "The min value should be greater than or equal to the max value");
589            } else if (min == NOT_INITIALIZED) {
590                throw new IllegalArgumentException(
591                        "The min value should be greater than Integer.MIN_VALUE.");
592            }
593            mMinValue = min;
594            mMaxValue = max;
595            mNextValue = mCurrentValue = NOT_INITIALIZED;
596            for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
597                mNumberViews[i].setText(i == CURRENT_NUMBER_VIEW_INDEX ? INITIAL_TEXT : "");
598            }
599            mBackgroundView.setText(INITIAL_TEXT);
600        }
601
602        void setPinDialogFragment(PinDialogFragment dlg) {
603            mDialog = dlg;
604        }
605
606        void setNextNumberPicker(PinNumberPicker picker) {
607            mNextNumberPicker = picker;
608        }
609
610        int getValue() {
611            if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) {
612                throw new IllegalStateException("Value is not set");
613            }
614            return mCurrentValue;
615        }
616
617        void updateFocus(boolean withAnimation) {
618            mScrollAnimatorSet.end();
619            mFocusGainAnimator.end();
620            mFocusLossAnimator.end();
621            updateText();
622            if (mNumberViewHolder.isFocused()) {
623                if (withAnimation) {
624                    mBackgroundView.setText(String.valueOf(mCurrentValue));
625                    mFocusGainAnimator.start();
626                } else {
627                    mBackgroundView.setAlpha(1f);
628                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(sAlphaForAdjacentNumber);
629                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(sAlphaForAdjacentNumber);
630                }
631            } else {
632                if (withAnimation) {
633                    mFocusLossAnimator.start();
634                } else {
635                    mBackgroundView.setAlpha(0f);
636                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(0f);
637                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(0f);
638                }
639                mNumberViewHolder.setScrollY(mNumberViewHeight);
640            }
641        }
642
643        private void updateText() {
644            boolean wasNotInitialized = false;
645            if (mNumberViewHolder.isFocused() && mCurrentValue == NOT_INITIALIZED) {
646                mNextValue = mCurrentValue = mMinValue;
647                wasNotInitialized = true;
648            }
649            if (mCurrentValue >= mMinValue && mCurrentValue <= mMaxValue) {
650                for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
651                    if (wasNotInitialized && i == CURRENT_NUMBER_VIEW_INDEX) {
652                        // In order to show the text change animation, keep the text of
653                        // mNumberViews[CURRENT_NUMBER_VIEW_INDEX].
654                    } else {
655                        mNumberViews[i].setText(String.valueOf(adjustValueInValidRange(
656                                mCurrentValue - CURRENT_NUMBER_VIEW_INDEX + i)));
657                    }
658                }
659            }
660        }
661
662        private int adjustValueInValidRange(int value) {
663            int interval = mMaxValue - mMinValue + 1;
664            if (value < mMinValue - interval || value > mMaxValue + interval) {
665                throw new IllegalArgumentException("The value( " + value
666                        + ") is too small or too big to adjust");
667            }
668            return (value < mMinValue) ? value + interval
669                    : (value > mMaxValue) ? value - interval : value;
670        }
671    }
672}
673