Home | History | Annotate | Download | only in dialog
      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 
    108     public static PinDialogFragment create(int type) {
    109         return create(type, null);
    110     }
    111 
    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
    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
    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
    148     public String getTrackerLabel() {
    149         return TRACKER_LABEL;
    150     }
    151 
    152     @Override
    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
    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 
    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