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