• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.ActivityManager;
20 import android.app.Dialog;
21 import android.content.DialogInterface;
22 import android.content.SharedPreferences;
23 import android.media.tv.TvContentRating;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.preference.PreferenceManager;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.ViewGroup.LayoutParams;
33 import android.widget.TextView;
34 import android.widget.Toast;
35 import com.android.tv.R;
36 import com.android.tv.TvSingletons;
37 import com.android.tv.common.SoftPreconditions;
38 import com.android.tv.dialog.picker.PinPicker;
39 import com.android.tv.util.TvSettings;
40 
41 public class PinDialogFragment extends SafeDismissDialogFragment {
42     private static final String TAG = "PinDialogFragment";
43     private static final boolean DEBUG = false;
44 
45     /** PIN code dialog for unlock channel */
46     public static final int PIN_DIALOG_TYPE_UNLOCK_CHANNEL = 0;
47 
48     /**
49      * PIN code dialog for unlock content. Only difference between {@code
50      * PIN_DIALOG_TYPE_UNLOCK_CHANNEL} is it's title.
51      */
52     public static final int PIN_DIALOG_TYPE_UNLOCK_PROGRAM = 1;
53 
54     /** PIN code dialog for change parental control settings */
55     public static final int PIN_DIALOG_TYPE_ENTER_PIN = 2;
56 
57     /** PIN code dialog for set new PIN */
58     public static final int PIN_DIALOG_TYPE_NEW_PIN = 3;
59 
60     // PIN code dialog for checking old PIN. Only used in this class.
61     private static final int PIN_DIALOG_TYPE_OLD_PIN = 4;
62 
63     /** PIN code dialog for unlocking DVR playback */
64     public static final int PIN_DIALOG_TYPE_UNLOCK_DVR = 5;
65 
66     private static final int MAX_WRONG_PIN_COUNT = 5;
67     private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute
68 
69     private static final String TRACKER_LABEL = "Pin dialog";
70     private static final String ARGS_TYPE = "args_type";
71     private static final String ARGS_RATING = "args_rating";
72 
73     public static final String DIALOG_TAG = PinDialogFragment.class.getName();
74 
75     private int mType;
76     private int mRequestType;
77     private boolean mPinChecked;
78     private boolean mDismissSilently;
79 
80     private TextView mWrongPinView;
81     private View mEnterPinView;
82     private TextView mTitleView;
83     private PinPicker mPicker;
84     private SharedPreferences mSharedPreferences;
85     private String mPrevPin;
86     private String mPin;
87     private String mRatingString;
88     private int mWrongPinCount;
89     private long mDisablePinUntil;
90     private final Handler mHandler = new Handler();
91 
create(int type)92     public static PinDialogFragment create(int type) {
93         return create(type, null);
94     }
95 
create(int type, String rating)96     public static PinDialogFragment create(int type, String rating) {
97         PinDialogFragment fragment = new PinDialogFragment();
98         Bundle args = new Bundle();
99         args.putInt(ARGS_TYPE, type);
100         args.putString(ARGS_RATING, rating);
101         fragment.setArguments(args);
102         return fragment;
103     }
104 
105     @Override
onCreate(Bundle savedInstanceState)106     public void onCreate(Bundle savedInstanceState) {
107         super.onCreate(savedInstanceState);
108         mRequestType = getArguments().getInt(ARGS_TYPE, PIN_DIALOG_TYPE_ENTER_PIN);
109         mType = mRequestType;
110         mRatingString = getArguments().getString(ARGS_RATING);
111         setStyle(STYLE_NO_TITLE, 0);
112         mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
113         mDisablePinUntil = TvSettings.getDisablePinUntil(getActivity());
114         if (ActivityManager.isUserAMonkey()) {
115             // Skip PIN dialog half the time for monkeys
116             if (Math.random() < 0.5) {
117                 exit(true);
118             }
119         }
120         mPinChecked = false;
121     }
122 
123     @Override
onCreateDialog(Bundle savedInstanceState)124     public Dialog onCreateDialog(Bundle savedInstanceState) {
125         Dialog dlg = super.onCreateDialog(savedInstanceState);
126         dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation;
127         return dlg;
128     }
129 
130     @Override
getTrackerLabel()131     public String getTrackerLabel() {
132         return TRACKER_LABEL;
133     }
134 
135     @Override
onStart()136     public void onStart() {
137         super.onStart();
138         // Dialog size is determined by its windows size, not inflated view size.
139         // So apply view size to window after the DialogFragment.onStart() where dialog is shown.
140         Dialog dlg = getDialog();
141         if (dlg != null) {
142             dlg.getWindow()
143                     .setLayout(
144                             getResources().getDimensionPixelSize(R.dimen.pin_dialog_width),
145                             LayoutParams.WRAP_CONTENT);
146         }
147     }
148 
149     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)150     public View onCreateView(
151             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
152         final View v = inflater.inflate(R.layout.pin_dialog, container, false);
153 
154         mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin);
155         mEnterPinView = v.findViewById(R.id.enter_pin);
156         mTitleView = (TextView) mEnterPinView.findViewById(R.id.title);
157         mPicker = v.findViewById(R.id.pin_picker);
158         mPicker.setOnClickListener(
159                 view -> {
160                     String pin = getPinInput();
161                     if (!TextUtils.isEmpty(pin)) {
162                         done(pin);
163                     }
164                 });
165         if (TextUtils.isEmpty(getPin())) {
166             // If PIN isn't set, user should set a PIN.
167             // Successfully setting a new set is considered as entering correct PIN.
168             mType = PIN_DIALOG_TYPE_NEW_PIN;
169         }
170         switch (mType) {
171             case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
172                 mTitleView.setText(R.string.pin_enter_unlock_channel);
173                 break;
174             case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
175                 mTitleView.setText(R.string.pin_enter_unlock_program);
176                 break;
177             case PIN_DIALOG_TYPE_UNLOCK_DVR:
178                 TvContentRating tvContentRating =
179                         TvContentRating.unflattenFromString(mRatingString);
180                 if (TvContentRating.UNRATED.equals(tvContentRating)) {
181                     mTitleView.setText(getString(R.string.pin_enter_unlock_dvr_unrated));
182                 } else {
183                     mTitleView.setText(
184                             getString(
185                                     R.string.pin_enter_unlock_dvr,
186                                     TvSingletons.getSingletons(getContext())
187                                             .getTvInputManagerHelper()
188                                             .getContentRatingsManager()
189                                             .getDisplayNameForRating(tvContentRating)));
190                 }
191                 break;
192             case PIN_DIALOG_TYPE_ENTER_PIN:
193                 mTitleView.setText(R.string.pin_enter_pin);
194                 break;
195             case PIN_DIALOG_TYPE_NEW_PIN:
196                 if (TextUtils.isEmpty(getPin())) {
197                     mTitleView.setText(R.string.pin_enter_create_pin);
198                 } else {
199                     mTitleView.setText(R.string.pin_enter_old_pin);
200                     mType = PIN_DIALOG_TYPE_OLD_PIN;
201                 }
202         }
203 
204         if (mType != PIN_DIALOG_TYPE_NEW_PIN) {
205             updateWrongPin();
206         }
207         mPicker.requestFocus();
208         return v;
209     }
210 
updateWrongPin()211     private void updateWrongPin() {
212         if (getActivity() == null) {
213             // The activity is already detached. No need to update.
214             mHandler.removeCallbacks(null);
215             return;
216         }
217 
218         int remainingSeconds = (int) ((mDisablePinUntil - System.currentTimeMillis()) / 1000);
219         boolean enabled = remainingSeconds < 1;
220         if (enabled) {
221             mWrongPinView.setVisibility(View.INVISIBLE);
222             mEnterPinView.setVisibility(View.VISIBLE);
223             mWrongPinCount = 0;
224         } else {
225             mEnterPinView.setVisibility(View.INVISIBLE);
226             mWrongPinView.setVisibility(View.VISIBLE);
227             mWrongPinView.setText(
228                     getResources()
229                             .getQuantityString(
230                                     R.plurals.pin_enter_countdown,
231                                     remainingSeconds,
232                                     remainingSeconds));
233 
234             mHandler.postDelayed(this::updateWrongPin, 1000);
235         }
236     }
237 
238     private void exit(boolean pinChecked) {
239         mPinChecked = pinChecked;
240         dismiss();
241     }
242 
243     /** Dismisses the pin dialog without calling activity listener. */
244     public void dismissSilently() {
245         mDismissSilently = true;
246         dismiss();
247     }
248 
249     @Override
250     public void onDismiss(DialogInterface dialog) {
251         super.onDismiss(dialog);
252         if (DEBUG) Log.d(TAG, "onDismiss: mPinChecked=" + mPinChecked);
253         SoftPreconditions.checkState(getActivity() instanceof OnPinCheckedListener);
254         if (!mDismissSilently && getActivity() instanceof OnPinCheckedListener) {
255             ((OnPinCheckedListener) getActivity())
256                     .onPinChecked(mPinChecked, mRequestType, mRatingString);
257         }
258         mDismissSilently = false;
259     }
260 
261     private void handleWrongPin() {
262         if (++mWrongPinCount >= MAX_WRONG_PIN_COUNT) {
263             mDisablePinUntil = System.currentTimeMillis() + DISABLE_PIN_DURATION_MILLIS;
264             TvSettings.setDisablePinUntil(getActivity(), mDisablePinUntil);
265             updateWrongPin();
266         } else {
267             showToast(R.string.pin_toast_wrong);
268         }
269     }
270 
271     private void showToast(int resId) {
272         Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show();
273     }
274 
275     private void done(String pin) {
276         if (DEBUG) Log.d(TAG, "done: mType=" + mType + " pin=" + pin + " stored=" + getPin());
277         switch (mType) {
278             case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
279             case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
280             case PIN_DIALOG_TYPE_UNLOCK_DVR:
281             case PIN_DIALOG_TYPE_ENTER_PIN:
282                 if (TextUtils.isEmpty(getPin()) || pin.equals(getPin())) {
283                     exit(true);
284                 } else {
285                     resetPinInput();
286                     handleWrongPin();
287                 }
288                 break;
289             case PIN_DIALOG_TYPE_NEW_PIN:
290                 resetPinInput();
291                 if (mPrevPin == null) {
292                     mPrevPin = pin;
293                     mTitleView.setText(R.string.pin_enter_again);
294                 } else {
295                     if (pin.equals(mPrevPin)) {
296                         setPin(pin);
297                         exit(true);
298                     } else {
299                         if (TextUtils.isEmpty(getPin())) {
300                             mTitleView.setText(R.string.pin_enter_create_pin);
301                         } else {
302                             mTitleView.setText(R.string.pin_enter_new_pin);
303                         }
304                         mPrevPin = null;
305                         showToast(R.string.pin_toast_not_match);
306                     }
307                 }
308                 break;
309             case PIN_DIALOG_TYPE_OLD_PIN:
310                 // Call resetPinInput() here because we'll get additional PIN input
311                 // regardless of the result.
312                 resetPinInput();
313                 if (pin.equals(getPin())) {
314                     mType = PIN_DIALOG_TYPE_NEW_PIN;
315                     mTitleView.setText(R.string.pin_enter_new_pin);
316                 } else {
317                     handleWrongPin();
318                 }
319                 break;
320         }
321     }
322 
323     public int getType() {
324         return mType;
325     }
326 
327     private void setPin(String pin) {
328         if (DEBUG) Log.d(TAG, "setPin: " + pin);
329         mPin = pin;
330         mSharedPreferences.edit().putString(TvSettings.PREF_PIN, pin).apply();
331     }
332 
333     private String getPin() {
334         if (mPin == null) {
335             mPin = mSharedPreferences.getString(TvSettings.PREF_PIN, "");
336         }
337         return mPin;
338     }
339 
340     private String getPinInput() {
341         return mPicker.getPinInput();
342     }
343 
344     private void resetPinInput() {
345         mPicker.resetPinInput();
346     }
347 
348     /**
349      * A listener to the result of {@link PinDialogFragment}. Any activity requiring pin code
350      * checking should implement this listener to receive the result.
351      */
352     public interface OnPinCheckedListener {
353         /**
354          * Called when {@link PinDialogFragment} is dismissed.
355          *
356          * @param checked {@code true} if the pin code entered is checked to be correct, otherwise
357          *     {@code false}.
358          * @param type The dialog type regarding to what pin entering is for.
359          * @param rating The target rating to unblock for.
360          */
361         void onPinChecked(boolean checked, int type, String rating);
362     }
363 }
364