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