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