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