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