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