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