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