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