• 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     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 {
done(boolean success)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 
PinDialogFragment(int type, ResultListener listener)111     public PinDialogFragment(int type, ResultListener listener) {
112         mType = type;
113         mListener = listener;
114         mRetCode = PIN_DIALOG_RESULT_FAIL;
115     }
116 
117     @Override
onCreate(Bundle savedInstanceState)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
onCreateDialog(Bundle savedInstanceState)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
getTrackerLabel()140     public String getTrackerLabel() {
141         return TRACKER_LABEL;
142     }
143 
144     @Override
onStart()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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 
setResultListener(ResultListener listener)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 
updateWrongPin()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