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