Home | History | Annotate | Download | only in dialog
      1 /*
      2  * Copyright (C) 2014 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.app.Dialog;
     22 import android.content.Context;
     23 import android.content.DialogInterface;
     24 import android.content.SharedPreferences;
     25 import android.content.res.Resources;
     26 import android.os.Bundle;
     27 import android.os.Handler;
     28 import android.preference.PreferenceManager;
     29 import android.text.TextUtils;
     30 import android.text.format.DateUtils;
     31 import android.util.AttributeSet;
     32 import android.util.Log;
     33 import android.util.TypedValue;
     34 import android.view.KeyEvent;
     35 import android.view.LayoutInflater;
     36 import android.view.View;
     37 import android.view.ViewGroup;
     38 import android.widget.FrameLayout;
     39 import android.widget.OverScroller;
     40 import android.widget.TextView;
     41 import android.widget.Toast;
     42 
     43 import com.android.tv.settings.R;
     44 
     45 public abstract class PinDialogFragment extends SafeDismissDialogFragment {
     46     private static final String TAG = "PinDialogFragment";
     47     private static boolean DEBUG = false;
     48 
     49     /**
     50      * PIN code dialog for unlock channel
     51      */
     52     public static final int PIN_DIALOG_TYPE_UNLOCK_CHANNEL = 0;
     53 
     54     /**
     55      * PIN code dialog for unlock content.
     56      * Only difference between {@code PIN_DIALOG_TYPE_UNLOCK_CHANNEL} is it's title.
     57      */
     58     public static final int PIN_DIALOG_TYPE_UNLOCK_PROGRAM = 1;
     59 
     60     /**
     61      * PIN code dialog for change parental control settings
     62      */
     63     public static final int PIN_DIALOG_TYPE_ENTER_PIN = 2;
     64 
     65     /**
     66      * PIN code dialog for set new PIN
     67      */
     68     public static final int PIN_DIALOG_TYPE_NEW_PIN = 3;
     69 
     70     // PIN code dialog for checking old PIN. This is intenal only.
     71     private static final int PIN_DIALOG_TYPE_OLD_PIN = 4;
     72 
     73     private static final int PIN_DIALOG_RESULT_SUCCESS = 0;
     74     private static final int PIN_DIALOG_RESULT_FAIL = 1;
     75 
     76     private static final int MAX_WRONG_PIN_COUNT = 5;
     77     private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute
     78 
     79     public interface ResultListener {
     80         void done(boolean success);
     81     }
     82 
     83     public static final String DIALOG_TAG = PinDialogFragment.class.getName();
     84 
     85     private static final int NUMBER_PICKERS_RES_ID[] = {
     86             R.id.first, R.id.second, R.id.third, R.id.fourth };
     87 
     88     private int mType;
     89     private final ResultListener mListener;
     90     private int mRetCode;
     91 
     92     private TextView mWrongPinView;
     93     private View mEnterPinView;
     94     private TextView mTitleView;
     95     private PinNumberPicker[] mPickers;
     96     private String mPrevPin;
     97     private int mWrongPinCount;
     98     private long mDisablePinUntil;
     99     private final Handler mHandler = new Handler();
    100 
    101     public abstract long getPinDisabledUntil();
    102     public abstract void setPinDisabledUntil(long retryDisableTimeout);
    103     public abstract void setPin(String pin);
    104     public abstract boolean isPinCorrect(String pin);
    105     public abstract boolean isPinSet();
    106 
    107     public PinDialogFragment(int type, ResultListener listener) {
    108         mType = type;
    109         mListener = listener;
    110         mRetCode = PIN_DIALOG_RESULT_FAIL;
    111     }
    112 
    113     @Override
    114     public void onCreate(Bundle savedInstanceState) {
    115         super.onCreate(savedInstanceState);
    116         setStyle(STYLE_NO_TITLE, 0);
    117         mDisablePinUntil = getPinDisabledUntil();
    118     }
    119 
    120     @Override
    121     public Dialog onCreateDialog(Bundle savedInstanceState) {
    122         Dialog dlg = super.onCreateDialog(savedInstanceState);
    123         dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation;
    124         PinNumberPicker.loadResources(dlg.getContext());
    125         return dlg;
    126     }
    127 
    128     @Override
    129     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    130             Bundle savedInstanceState) {
    131         final View v = inflater.inflate(R.layout.pin_dialog, container, false);
    132 
    133         mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin);
    134         mEnterPinView = v.findViewById(R.id.enter_pin);
    135         mTitleView = (TextView) mEnterPinView.findViewById(R.id.title);
    136         if (!isPinSet()) {
    137             // If PIN isn't set, user should set a PIN.
    138             // Successfully setting a new set is considered as entering correct PIN.
    139             mType = PIN_DIALOG_TYPE_NEW_PIN;
    140         }
    141         switch (mType) {
    142             case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
    143                 mTitleView.setText(R.string.pin_enter_unlock_channel);
    144                 break;
    145             case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
    146                 mTitleView.setText(R.string.pin_enter_unlock_program);
    147                 break;
    148             case PIN_DIALOG_TYPE_ENTER_PIN:
    149                 mTitleView.setText(R.string.pin_enter_pin);
    150                 break;
    151             case PIN_DIALOG_TYPE_NEW_PIN:
    152                 if (!isPinSet()) {
    153                     mTitleView.setText(R.string.pin_enter_new_pin);
    154                 } else {
    155                     mTitleView.setText(R.string.pin_enter_old_pin);
    156                     mType = PIN_DIALOG_TYPE_OLD_PIN;
    157                 }
    158         }
    159 
    160         mPickers = new PinNumberPicker[NUMBER_PICKERS_RES_ID.length];
    161         for (int i = 0; i < NUMBER_PICKERS_RES_ID.length; i++) {
    162             mPickers[i] = (PinNumberPicker) v.findViewById(NUMBER_PICKERS_RES_ID[i]);
    163             mPickers[i].setValueRange(0, 9);
    164             mPickers[i].setPinDialogFragment(this);
    165             mPickers[i].updateFocus();
    166         }
    167         for (int i = 0; i < NUMBER_PICKERS_RES_ID.length - 1; i++) {
    168             mPickers[i].setNextNumberPicker(mPickers[i + 1]);
    169         }
    170 
    171         if (mType != PIN_DIALOG_TYPE_NEW_PIN) {
    172             updateWrongPin();
    173         }
    174         return v;
    175     }
    176 
    177     private final Runnable mUpdateEnterPinRunnable = new Runnable() {
    178         @Override
    179         public void run() {
    180             updateWrongPin();
    181         }
    182     };
    183 
    184     private void updateWrongPin() {
    185         if (getActivity() == null) {
    186             // The activity is already detached. No need to update.
    187             mHandler.removeCallbacks(null);
    188             return;
    189         }
    190 
    191         boolean enabled = (mDisablePinUntil - System.currentTimeMillis()) / 1000 < 1;
    192         if (enabled) {
    193             mWrongPinView.setVisibility(View.INVISIBLE);
    194             mEnterPinView.setVisibility(View.VISIBLE);
    195             mWrongPinCount = 0;
    196         } else {
    197             mEnterPinView.setVisibility(View.INVISIBLE);
    198             mWrongPinView.setVisibility(View.VISIBLE);
    199             mWrongPinView.setText(getResources().getString(R.string.pin_enter_wrong,
    200                     DateUtils.getRelativeTimeSpanString(mDisablePinUntil,
    201                             System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS)));
    202             mHandler.postDelayed(mUpdateEnterPinRunnable, 1000);
    203         }
    204     }
    205 
    206     private void exit(int retCode) {
    207         mRetCode = retCode;
    208         dismiss();
    209     }
    210 
    211     @Override
    212     public void onDismiss(DialogInterface dialog) {
    213         super.onDismiss(dialog);
    214         if (DEBUG) Log.d(TAG, "onDismiss: mRetCode=" + mRetCode);
    215         if (mListener != null) {
    216             mListener.done(mRetCode == PIN_DIALOG_RESULT_SUCCESS);
    217         }
    218     }
    219 
    220     private void handleWrongPin() {
    221         if (++mWrongPinCount >= MAX_WRONG_PIN_COUNT) {
    222             mDisablePinUntil = System.currentTimeMillis() + DISABLE_PIN_DURATION_MILLIS;
    223             setPinDisabledUntil(mDisablePinUntil);
    224             updateWrongPin();
    225         } else {
    226             showToast(R.string.pin_toast_wrong);
    227         }
    228     }
    229 
    230     private void showToast(int resId) {
    231         Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show();
    232     }
    233 
    234     private void done(String pin) {
    235         if (DEBUG) Log.d(TAG, "done: mType=" + mType + " pin=" + pin);
    236         switch (mType) {
    237             case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
    238             case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
    239             case PIN_DIALOG_TYPE_ENTER_PIN:
    240                 // TODO: Implement limited number of retrials and timeout logic.
    241                 if (!isPinSet() || isPinCorrect(pin)) {
    242                     exit(PIN_DIALOG_RESULT_SUCCESS);
    243                 } else {
    244                     resetPinInput();
    245                     handleWrongPin();
    246                 }
    247                 break;
    248             case PIN_DIALOG_TYPE_NEW_PIN:
    249                 resetPinInput();
    250                 if (mPrevPin == null) {
    251                     mPrevPin = pin;
    252                     mTitleView.setText(R.string.pin_enter_again);
    253                 } else {
    254                     if (pin.equals(mPrevPin)) {
    255                         setPin(pin);
    256                         exit(PIN_DIALOG_RESULT_SUCCESS);
    257                     } else {
    258                         mTitleView.setText(R.string.pin_enter_new_pin);
    259                         mPrevPin = null;
    260                         showToast(R.string.pin_toast_not_match);
    261                     }
    262                 }
    263                 break;
    264             case PIN_DIALOG_TYPE_OLD_PIN:
    265                 if (isPinCorrect(pin)) {
    266                     mType = PIN_DIALOG_TYPE_NEW_PIN;
    267                     resetPinInput();
    268                     mTitleView.setText(R.string.pin_enter_new_pin);
    269                 } else {
    270                     handleWrongPin();
    271                 }
    272                 break;
    273         }
    274     }
    275 
    276     public int getType() {
    277         return mType;
    278     }
    279 
    280     private String getPinInput() {
    281         String result = "";
    282         try {
    283             for (PinNumberPicker pnp : mPickers) {
    284                 pnp.updateText();
    285                 result += pnp.getValue();
    286             }
    287         } catch (IllegalStateException e) {
    288             result = "";
    289         }
    290         return result;
    291     }
    292 
    293     private void resetPinInput() {
    294         for (PinNumberPicker pnp : mPickers) {
    295             pnp.setValueRange(0, 9);
    296         }
    297         mPickers[0].requestFocus();
    298     }
    299 
    300     public static final class PinNumberPicker extends FrameLayout {
    301         private static final int NUMBER_VIEWS_RES_ID[] = {
    302             R.id.previous2_number,
    303             R.id.previous_number,
    304             R.id.current_number,
    305             R.id.next_number,
    306             R.id.next2_number };
    307         private static final int CURRENT_NUMBER_VIEW_INDEX = 2;
    308 
    309         private static Animator sFocusedNumberEnterAnimator;
    310         private static Animator sFocusedNumberExitAnimator;
    311         private static Animator sAdjacentNumberEnterAnimator;
    312         private static Animator sAdjacentNumberExitAnimator;
    313 
    314         private static float sAlphaForFocusedNumber;
    315         private static float sAlphaForAdjacentNumber;
    316 
    317         private int mMinValue;
    318         private int mMaxValue;
    319         private int mCurrentValue;
    320         private int mNextValue;
    321         private int mNumberViewHeight;
    322         private PinDialogFragment mDialog;
    323         private PinNumberPicker mNextNumberPicker;
    324         private boolean mCancelAnimation;
    325 
    326         private final View mNumberViewHolder;
    327         private final View mBackgroundView;
    328         private final TextView[] mNumberViews;
    329         private final OverScroller mScroller;
    330 
    331         public PinNumberPicker(Context context) {
    332             this(context, null);
    333         }
    334 
    335         public PinNumberPicker(Context context, AttributeSet attrs) {
    336             this(context, attrs, 0);
    337         }
    338 
    339         public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
    340             this(context, attrs, defStyleAttr, 0);
    341         }
    342 
    343         public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr,
    344                 int defStyleRes) {
    345             super(context, attrs, defStyleAttr, defStyleRes);
    346             View view = inflate(context, R.layout.pin_number_picker, this);
    347             mNumberViewHolder = view.findViewById(R.id.number_view_holder);
    348             mBackgroundView = view.findViewById(R.id.focused_background);
    349             mNumberViews = new TextView[NUMBER_VIEWS_RES_ID.length];
    350             for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
    351                 mNumberViews[i] = (TextView) view.findViewById(NUMBER_VIEWS_RES_ID[i]);
    352             }
    353             Resources resources = context.getResources();
    354             mNumberViewHeight = resources.getDimensionPixelOffset(
    355                     R.dimen.pin_number_picker_text_view_height);
    356 
    357             mScroller = new OverScroller(context);
    358 
    359             mNumberViewHolder.setOnFocusChangeListener(new OnFocusChangeListener() {
    360                 @Override
    361                 public void onFocusChange(View v, boolean hasFocus) {
    362                     updateFocus();
    363                 }
    364             });
    365 
    366             mNumberViewHolder.setOnKeyListener(new OnKeyListener() {
    367                 @Override
    368                 public boolean onKey(View v, int keyCode, KeyEvent event) {
    369                     if (event.getAction() == KeyEvent.ACTION_DOWN) {
    370                         switch (keyCode) {
    371                             case KeyEvent.KEYCODE_DPAD_UP:
    372                             case KeyEvent.KEYCODE_DPAD_DOWN: {
    373                                 if (!mScroller.isFinished() || mCancelAnimation) {
    374                                     endScrollAnimation();
    375                                 }
    376                                 if (mScroller.isFinished() || mCancelAnimation) {
    377                                     mCancelAnimation = false;
    378                                     if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
    379                                         mNextValue = adjustValueInValidRange(mCurrentValue + 1);
    380                                         startScrollAnimation(true);
    381                                         mScroller.startScroll(0, 0, 0, mNumberViewHeight,
    382                                                 getResources().getInteger(
    383                                                         R.integer.pin_number_scroll_duration));
    384                                     } else {
    385                                         mNextValue = adjustValueInValidRange(mCurrentValue - 1);
    386                                         startScrollAnimation(false);
    387                                         mScroller.startScroll(0, 0, 0, -mNumberViewHeight,
    388                                                 getResources().getInteger(
    389                                                         R.integer.pin_number_scroll_duration));
    390                                     }
    391                                     updateText();
    392                                     invalidate();
    393                                 }
    394                                 return true;
    395                             }
    396                         }
    397                     } else if (event.getAction() == KeyEvent.ACTION_UP) {
    398                         switch (keyCode) {
    399                             case KeyEvent.KEYCODE_DPAD_UP:
    400                             case KeyEvent.KEYCODE_DPAD_DOWN: {
    401                                 mCancelAnimation = true;
    402                                 return true;
    403                             }
    404                         }
    405                     }
    406                     return false;
    407                 }
    408             });
    409             mNumberViewHolder.setScrollY(mNumberViewHeight);
    410         }
    411 
    412         static void loadResources(Context context) {
    413             if (sFocusedNumberEnterAnimator == null) {
    414                 TypedValue outValue = new TypedValue();
    415                 context.getResources().getValue(
    416                         R.float_type.pin_alpha_for_focused_number, outValue, true);
    417                 sAlphaForFocusedNumber = outValue.getFloat();
    418                 context.getResources().getValue(
    419                         R.float_type.pin_alpha_for_adjacent_number, outValue, true);
    420                 sAlphaForAdjacentNumber = outValue.getFloat();
    421 
    422                 sFocusedNumberEnterAnimator = AnimatorInflater.loadAnimator(context,
    423                         R.animator.pin_focused_number_enter);
    424                 sFocusedNumberExitAnimator = AnimatorInflater.loadAnimator(context,
    425                         R.animator.pin_focused_number_exit);
    426                 sAdjacentNumberEnterAnimator = AnimatorInflater.loadAnimator(context,
    427                         R.animator.pin_adjacent_number_enter);
    428                 sAdjacentNumberExitAnimator = AnimatorInflater.loadAnimator(context,
    429                         R.animator.pin_adjacent_number_exit);
    430             }
    431         }
    432 
    433         @Override
    434         public void computeScroll() {
    435             super.computeScroll();
    436             if (mScroller.computeScrollOffset()) {
    437                 mNumberViewHolder.setScrollY(mScroller.getCurrY() + mNumberViewHeight);
    438                 updateText();
    439                 invalidate();
    440             } else if (mCurrentValue != mNextValue) {
    441                 mCurrentValue = mNextValue;
    442             }
    443         }
    444 
    445         @Override
    446         public boolean dispatchKeyEvent(KeyEvent event) {
    447             if (event.getAction() == KeyEvent.ACTION_UP) {
    448                 int keyCode = event.getKeyCode();
    449                 if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
    450                     setNextValue(keyCode - KeyEvent.KEYCODE_0);
    451                 } else if (keyCode != KeyEvent.KEYCODE_DPAD_CENTER
    452                         && keyCode != KeyEvent.KEYCODE_ENTER) {
    453                     return super.dispatchKeyEvent(event);
    454                 }
    455                 if (mNextNumberPicker == null) {
    456                     String pin = mDialog.getPinInput();
    457                     if (!TextUtils.isEmpty(pin)) {
    458                         mDialog.done(pin);
    459                     }
    460                 } else {
    461                     mNextNumberPicker.requestFocus();
    462                 }
    463                 return true;
    464             }
    465             return super.dispatchKeyEvent(event);
    466         }
    467 
    468         @Override
    469         public void setEnabled(boolean enabled) {
    470             super.setEnabled(enabled);
    471             mNumberViewHolder.setFocusable(enabled);
    472             for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
    473                 mNumberViews[i].setEnabled(enabled);
    474             }
    475         }
    476 
    477         void startScrollAnimation(boolean scrollUp) {
    478             if (scrollUp) {
    479                 sAdjacentNumberExitAnimator.setTarget(mNumberViews[1]);
    480                 sFocusedNumberExitAnimator.setTarget(mNumberViews[2]);
    481                 sFocusedNumberEnterAnimator.setTarget(mNumberViews[3]);
    482                 sAdjacentNumberEnterAnimator.setTarget(mNumberViews[4]);
    483             } else {
    484                 sAdjacentNumberEnterAnimator.setTarget(mNumberViews[0]);
    485                 sFocusedNumberEnterAnimator.setTarget(mNumberViews[1]);
    486                 sFocusedNumberExitAnimator.setTarget(mNumberViews[2]);
    487                 sAdjacentNumberExitAnimator.setTarget(mNumberViews[3]);
    488             }
    489             sAdjacentNumberExitAnimator.start();
    490             sFocusedNumberExitAnimator.start();
    491             sFocusedNumberEnterAnimator.start();
    492             sAdjacentNumberEnterAnimator.start();
    493         }
    494 
    495         void endScrollAnimation() {
    496             sAdjacentNumberExitAnimator.end();
    497             sFocusedNumberExitAnimator.end();
    498             sFocusedNumberEnterAnimator.end();
    499             sAdjacentNumberEnterAnimator.end();
    500             mCurrentValue = mNextValue;
    501             mNumberViews[1].setAlpha(sAlphaForAdjacentNumber);
    502             mNumberViews[2].setAlpha(sAlphaForFocusedNumber);
    503             mNumberViews[3].setAlpha(sAlphaForAdjacentNumber);
    504         }
    505 
    506         void setValueRange(int min, int max) {
    507             if (min > max) {
    508                 throw new IllegalArgumentException(
    509                         "The min value should be greater than or equal to the max value");
    510             }
    511             mMinValue = min;
    512             mMaxValue = max;
    513             mNextValue = mCurrentValue = mMinValue - 1;
    514             clearText();
    515             mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText("");
    516         }
    517 
    518         void setPinDialogFragment(PinDialogFragment dlg) {
    519             mDialog = dlg;
    520         }
    521 
    522         void setNextNumberPicker(PinNumberPicker picker) {
    523             mNextNumberPicker = picker;
    524         }
    525 
    526         int getValue() {
    527             if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) {
    528                 throw new IllegalStateException("Value is not set");
    529             }
    530             return mCurrentValue;
    531         }
    532 
    533         // Will take effect when the focus is updated.
    534         void setNextValue(int value) {
    535             if (value < mMinValue || value > mMaxValue) {
    536                 throw new IllegalStateException("Value is not set");
    537             }
    538             mNextValue = adjustValueInValidRange(value);
    539         }
    540 
    541         void updateFocus() {
    542             endScrollAnimation();
    543             if (mNumberViewHolder.isFocused()) {
    544                 mBackgroundView.setVisibility(View.VISIBLE);
    545                 updateText();
    546             } else {
    547                 mBackgroundView.setVisibility(View.GONE);
    548                 if (!mScroller.isFinished()) {
    549                     mCurrentValue = mNextValue;
    550                     mScroller.abortAnimation();
    551                 }
    552                 clearText();
    553                 mNumberViewHolder.setScrollY(mNumberViewHeight);
    554             }
    555         }
    556 
    557         private void clearText() {
    558             for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
    559                 if (i != CURRENT_NUMBER_VIEW_INDEX) {
    560                     mNumberViews[i].setText("");
    561                 } else if (mCurrentValue >= mMinValue && mCurrentValue <= mMaxValue) {
    562                     mNumberViews[i].setText(String.valueOf(mCurrentValue));
    563                 }
    564             }
    565         }
    566 
    567         private void updateText() {
    568             if (mNumberViewHolder.isFocused()) {
    569                 if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) {
    570                     mNextValue = mCurrentValue = mMinValue;
    571                 }
    572                 int value = adjustValueInValidRange(mCurrentValue - CURRENT_NUMBER_VIEW_INDEX);
    573                 for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
    574                     mNumberViews[i].setText(String.valueOf(adjustValueInValidRange(value)));
    575                     value = adjustValueInValidRange(value + 1);
    576                 }
    577             }
    578         }
    579 
    580         private int adjustValueInValidRange(int value) {
    581             int interval = mMaxValue - mMinValue + 1;
    582             if (value < mMinValue - interval || value > mMaxValue + interval) {
    583                 throw new IllegalArgumentException("The value( " + value
    584                         + ") is too small or too big to adjust");
    585             }
    586             return (value < mMinValue) ? value + interval
    587                     : (value > mMaxValue) ? value - interval : value;
    588         }
    589     }
    590 }
    591