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.animation.Animator; 20 import android.animation.AnimatorInflater; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.AnimatorSet; 23 import android.animation.ObjectAnimator; 24 import android.animation.ValueAnimator; 25 import android.app.ActivityManager; 26 import android.app.Dialog; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.SharedPreferences; 30 import android.content.res.Resources; 31 import android.media.tv.TvContentRating; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import android.preference.PreferenceManager; 35 import android.text.TextUtils; 36 import android.util.AttributeSet; 37 import android.util.Log; 38 import android.util.TypedValue; 39 import android.view.KeyEvent; 40 import android.view.LayoutInflater; 41 import android.view.View; 42 import android.view.ViewGroup; 43 import android.view.ViewGroup.LayoutParams; 44 import android.widget.FrameLayout; 45 import android.widget.TextView; 46 import android.widget.Toast; 47 import com.android.tv.R; 48 import com.android.tv.TvSingletons; 49 import com.android.tv.common.SoftPreconditions; 50 import com.android.tv.util.TvSettings; 51 52 public class PinDialogFragment extends SafeDismissDialogFragment { 53 private static final String TAG = "PinDialogFragment"; 54 private static final boolean DEBUG = false; 55 56 /** PIN code dialog for unlock channel */ 57 public static final int PIN_DIALOG_TYPE_UNLOCK_CHANNEL = 0; 58 59 /** 60 * PIN code dialog for unlock content. Only difference between {@code 61 * PIN_DIALOG_TYPE_UNLOCK_CHANNEL} is it's title. 62 */ 63 public static final int PIN_DIALOG_TYPE_UNLOCK_PROGRAM = 1; 64 65 /** PIN code dialog for change parental control settings */ 66 public static final int PIN_DIALOG_TYPE_ENTER_PIN = 2; 67 68 /** PIN code dialog for set new PIN */ 69 public static final int PIN_DIALOG_TYPE_NEW_PIN = 3; 70 71 // PIN code dialog for checking old PIN. Only used in this class. 72 private static final int PIN_DIALOG_TYPE_OLD_PIN = 4; 73 74 /** PIN code dialog for unlocking DVR playback */ 75 public static final int PIN_DIALOG_TYPE_UNLOCK_DVR = 5; 76 77 private static final int MAX_WRONG_PIN_COUNT = 5; 78 private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute 79 80 private static final String INITIAL_TEXT = ""; 81 private static final String TRACKER_LABEL = "Pin dialog"; 82 private static final String ARGS_TYPE = "args_type"; 83 private static final String ARGS_RATING = "args_rating"; 84 85 public static final String DIALOG_TAG = PinDialogFragment.class.getName(); 86 87 private static final int NUMBER_PICKERS_RES_ID[] = { 88 R.id.first, R.id.second, R.id.third, R.id.fourth 89 }; 90 91 private int mType; 92 private int mRequestType; 93 private boolean mPinChecked; 94 private boolean mDismissSilently; 95 96 private TextView mWrongPinView; 97 private View mEnterPinView; 98 private TextView mTitleView; 99 private PinNumberPicker[] mPickers; 100 private SharedPreferences mSharedPreferences; 101 private String mPrevPin; 102 private String mPin; 103 private String mRatingString; 104 private int mWrongPinCount; 105 private long mDisablePinUntil; 106 private final Handler mHandler = new Handler(); 107 108 public static PinDialogFragment create(int type) { 109 return create(type, null); 110 } 111 112 public static PinDialogFragment create(int type, String rating) { 113 PinDialogFragment fragment = new PinDialogFragment(); 114 Bundle args = new Bundle(); 115 args.putInt(ARGS_TYPE, type); 116 args.putString(ARGS_RATING, rating); 117 fragment.setArguments(args); 118 return fragment; 119 } 120 121 @Override 122 public void onCreate(Bundle savedInstanceState) { 123 super.onCreate(savedInstanceState); 124 mRequestType = getArguments().getInt(ARGS_TYPE, PIN_DIALOG_TYPE_ENTER_PIN); 125 mType = mRequestType; 126 mRatingString = getArguments().getString(ARGS_RATING); 127 setStyle(STYLE_NO_TITLE, 0); 128 mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); 129 mDisablePinUntil = TvSettings.getDisablePinUntil(getActivity()); 130 if (ActivityManager.isUserAMonkey()) { 131 // Skip PIN dialog half the time for monkeys 132 if (Math.random() < 0.5) { 133 exit(true); 134 } 135 } 136 mPinChecked = false; 137 } 138 139 @Override 140 public Dialog onCreateDialog(Bundle savedInstanceState) { 141 Dialog dlg = super.onCreateDialog(savedInstanceState); 142 dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation; 143 PinNumberPicker.loadResources(dlg.getContext()); 144 return dlg; 145 } 146 147 @Override 148 public String getTrackerLabel() { 149 return TRACKER_LABEL; 150 } 151 152 @Override 153 public void onStart() { 154 super.onStart(); 155 // Dialog size is determined by its windows size, not inflated view size. 156 // So apply view size to window after the DialogFragment.onStart() where dialog is shown. 157 Dialog dlg = getDialog(); 158 if (dlg != null) { 159 dlg.getWindow() 160 .setLayout( 161 getResources().getDimensionPixelSize(R.dimen.pin_dialog_width), 162 LayoutParams.WRAP_CONTENT); 163 } 164 } 165 166 @Override 167 public View onCreateView( 168 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 169 final View v = inflater.inflate(R.layout.pin_dialog, container, false); 170 171 mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin); 172 mEnterPinView = v.findViewById(R.id.enter_pin); 173 mTitleView = (TextView) mEnterPinView.findViewById(R.id.title); 174 if (TextUtils.isEmpty(getPin())) { 175 // If PIN isn't set, user should set a PIN. 176 // Successfully setting a new set is considered as entering correct PIN. 177 mType = PIN_DIALOG_TYPE_NEW_PIN; 178 } 179 switch (mType) { 180 case PIN_DIALOG_TYPE_UNLOCK_CHANNEL: 181 mTitleView.setText(R.string.pin_enter_unlock_channel); 182 break; 183 case PIN_DIALOG_TYPE_UNLOCK_PROGRAM: 184 mTitleView.setText(R.string.pin_enter_unlock_program); 185 break; 186 case PIN_DIALOG_TYPE_UNLOCK_DVR: 187 TvContentRating tvContentRating = 188 TvContentRating.unflattenFromString(mRatingString); 189 if (TvContentRating.UNRATED.equals(tvContentRating)) { 190 mTitleView.setText(getString(R.string.pin_enter_unlock_dvr_unrated)); 191 } else { 192 mTitleView.setText( 193 getString( 194 R.string.pin_enter_unlock_dvr, 195 TvSingletons.getSingletons(getContext()) 196 .getTvInputManagerHelper() 197 .getContentRatingsManager() 198 .getDisplayNameForRating(tvContentRating))); 199 } 200 break; 201 case PIN_DIALOG_TYPE_ENTER_PIN: 202 mTitleView.setText(R.string.pin_enter_pin); 203 break; 204 case PIN_DIALOG_TYPE_NEW_PIN: 205 if (TextUtils.isEmpty(getPin())) { 206 mTitleView.setText(R.string.pin_enter_create_pin); 207 } else { 208 mTitleView.setText(R.string.pin_enter_old_pin); 209 mType = PIN_DIALOG_TYPE_OLD_PIN; 210 } 211 } 212 213 mPickers = new PinNumberPicker[NUMBER_PICKERS_RES_ID.length]; 214 for (int i = 0; i < NUMBER_PICKERS_RES_ID.length; i++) { 215 mPickers[i] = (PinNumberPicker) v.findViewById(NUMBER_PICKERS_RES_ID[i]); 216 mPickers[i].setValueRangeAndResetText(0, 9); 217 mPickers[i].setPinDialogFragment(this); 218 mPickers[i].updateFocus(false); 219 } 220 for (int i = 0; i < NUMBER_PICKERS_RES_ID.length - 1; i++) { 221 mPickers[i].setNextNumberPicker(mPickers[i + 1]); 222 } 223 224 if (mType != PIN_DIALOG_TYPE_NEW_PIN) { 225 updateWrongPin(); 226 } 227 return v; 228 } 229 230 private final Runnable mUpdateEnterPinRunnable = 231 new Runnable() { 232 @Override 233 public void run() { 234 updateWrongPin(); 235 } 236 }; 237 238 private void updateWrongPin() { 239 if (getActivity() == null) { 240 // The activity is already detached. No need to update. 241 mHandler.removeCallbacks(null); 242 return; 243 } 244 245 int remainingSeconds = (int) ((mDisablePinUntil - System.currentTimeMillis()) / 1000); 246 boolean enabled = remainingSeconds < 1; 247 if (enabled) { 248 mWrongPinView.setVisibility(View.INVISIBLE); 249 mEnterPinView.setVisibility(View.VISIBLE); 250 mWrongPinCount = 0; 251 } else { 252 mEnterPinView.setVisibility(View.INVISIBLE); 253 mWrongPinView.setVisibility(View.VISIBLE); 254 mWrongPinView.setText( 255 getResources() 256 .getQuantityString( 257 R.plurals.pin_enter_countdown, 258 remainingSeconds, 259 remainingSeconds)); 260 mHandler.postDelayed(mUpdateEnterPinRunnable, 1000); 261 } 262 } 263 264 private void exit(boolean pinChecked) { 265 mPinChecked = pinChecked; 266 dismiss(); 267 } 268 269 /** Dismisses the pin dialog without calling activity listener. */ 270 public void dismissSilently() { 271 mDismissSilently = true; 272 dismiss(); 273 } 274 275 @Override 276 public void onDismiss(DialogInterface dialog) { 277 super.onDismiss(dialog); 278 if (DEBUG) Log.d(TAG, "onDismiss: mPinChecked=" + mPinChecked); 279 SoftPreconditions.checkState(getActivity() instanceof OnPinCheckedListener); 280 if (!mDismissSilently && getActivity() instanceof OnPinCheckedListener) { 281 ((OnPinCheckedListener) getActivity()) 282 .onPinChecked(mPinChecked, mRequestType, mRatingString); 283 } 284 mDismissSilently = false; 285 } 286 287 private void handleWrongPin() { 288 if (++mWrongPinCount >= MAX_WRONG_PIN_COUNT) { 289 mDisablePinUntil = System.currentTimeMillis() + DISABLE_PIN_DURATION_MILLIS; 290 TvSettings.setDisablePinUntil(getActivity(), mDisablePinUntil); 291 updateWrongPin(); 292 } else { 293 showToast(R.string.pin_toast_wrong); 294 } 295 } 296 297 private void showToast(int resId) { 298 Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show(); 299 } 300 301 private void done(String pin) { 302 if (DEBUG) Log.d(TAG, "done: mType=" + mType + " pin=" + pin + " stored=" + getPin()); 303 switch (mType) { 304 case PIN_DIALOG_TYPE_UNLOCK_CHANNEL: 305 case PIN_DIALOG_TYPE_UNLOCK_PROGRAM: 306 case PIN_DIALOG_TYPE_UNLOCK_DVR: 307 case PIN_DIALOG_TYPE_ENTER_PIN: 308 if (TextUtils.isEmpty(getPin()) || pin.equals(getPin())) { 309 exit(true); 310 } else { 311 resetPinInput(); 312 handleWrongPin(); 313 } 314 break; 315 case PIN_DIALOG_TYPE_NEW_PIN: 316 resetPinInput(); 317 if (mPrevPin == null) { 318 mPrevPin = pin; 319 mTitleView.setText(R.string.pin_enter_again); 320 } else { 321 if (pin.equals(mPrevPin)) { 322 setPin(pin); 323 exit(true); 324 } else { 325 if (TextUtils.isEmpty(getPin())) { 326 mTitleView.setText(R.string.pin_enter_create_pin); 327 } else { 328 mTitleView.setText(R.string.pin_enter_new_pin); 329 } 330 mPrevPin = null; 331 showToast(R.string.pin_toast_not_match); 332 } 333 } 334 break; 335 case PIN_DIALOG_TYPE_OLD_PIN: 336 // Call resetPinInput() here because we'll get additional PIN input 337 // regardless of the result. 338 resetPinInput(); 339 if (pin.equals(getPin())) { 340 mType = PIN_DIALOG_TYPE_NEW_PIN; 341 mTitleView.setText(R.string.pin_enter_new_pin); 342 } else { 343 handleWrongPin(); 344 } 345 break; 346 } 347 } 348 349 public int getType() { 350 return mType; 351 } 352 353 private void setPin(String pin) { 354 if (DEBUG) Log.d(TAG, "setPin: " + pin); 355 mPin = pin; 356 mSharedPreferences.edit().putString(TvSettings.PREF_PIN, pin).apply(); 357 } 358 359 private String getPin() { 360 if (mPin == null) { 361 mPin = mSharedPreferences.getString(TvSettings.PREF_PIN, ""); 362 } 363 return mPin; 364 } 365 366 private String getPinInput() { 367 String result = ""; 368 try { 369 for (PinNumberPicker pnp : mPickers) { 370 pnp.updateText(); 371 result += pnp.getValue(); 372 } 373 } catch (IllegalStateException e) { 374 result = ""; 375 } 376 return result; 377 } 378 379 private void resetPinInput() { 380 for (PinNumberPicker pnp : mPickers) { 381 pnp.setValueRangeAndResetText(0, 9); 382 } 383 mPickers[0].requestFocus(); 384 } 385 386 public static class PinNumberPicker extends FrameLayout { 387 private static final int NUMBER_VIEWS_RES_ID[] = { 388 R.id.previous2_number, 389 R.id.previous_number, 390 R.id.current_number, 391 R.id.next_number, 392 R.id.next2_number 393 }; 394 private static final int CURRENT_NUMBER_VIEW_INDEX = 2; 395 private static final int NOT_INITIALIZED = Integer.MIN_VALUE; 396 397 private static Animator sFocusedNumberEnterAnimator; 398 private static Animator sFocusedNumberExitAnimator; 399 private static Animator sAdjacentNumberEnterAnimator; 400 private static Animator sAdjacentNumberExitAnimator; 401 402 private static float sAlphaForFocusedNumber; 403 private static float sAlphaForAdjacentNumber; 404 405 private int mMinValue; 406 private int mMaxValue; 407 private int mCurrentValue; 408 // a value for setting mCurrentValue at the end of scroll animation. 409 private int mNextValue; 410 private final int mNumberViewHeight; 411 private PinDialogFragment mDialog; 412 private PinNumberPicker mNextNumberPicker; 413 private boolean mCancelAnimation; 414 415 private final View mNumberViewHolder; 416 // When the PinNumberPicker has focus, mBackgroundView will show the focused background. 417 // Also, this view is used for handling the text change animation of the current number 418 // view which is required when the current number view text is changing from INITIAL_TEXT 419 // to "0". 420 private final TextView mBackgroundView; 421 private final TextView[] mNumberViews; 422 private final AnimatorSet mFocusGainAnimator; 423 private final AnimatorSet mFocusLossAnimator; 424 private final AnimatorSet mScrollAnimatorSet; 425 426 public PinNumberPicker(Context context) { 427 this(context, null); 428 } 429 430 public PinNumberPicker(Context context, AttributeSet attrs) { 431 this(context, attrs, 0); 432 } 433 434 public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) { 435 this(context, attrs, defStyleAttr, 0); 436 } 437 438 public PinNumberPicker( 439 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 440 super(context, attrs, defStyleAttr, defStyleRes); 441 View view = inflate(context, R.layout.pin_number_picker, this); 442 mNumberViewHolder = view.findViewById(R.id.number_view_holder); 443 mBackgroundView = (TextView) view.findViewById(R.id.focused_background); 444 mNumberViews = new TextView[NUMBER_VIEWS_RES_ID.length]; 445 for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { 446 mNumberViews[i] = (TextView) view.findViewById(NUMBER_VIEWS_RES_ID[i]); 447 } 448 Resources resources = context.getResources(); 449 mNumberViewHeight = 450 resources.getDimensionPixelSize(R.dimen.pin_number_picker_text_view_height); 451 452 mNumberViewHolder.setOnFocusChangeListener( 453 new OnFocusChangeListener() { 454 @Override 455 public void onFocusChange(View v, boolean hasFocus) { 456 updateFocus(true); 457 } 458 }); 459 460 mNumberViewHolder.setOnKeyListener( 461 new OnKeyListener() { 462 @Override 463 public boolean onKey(View v, int keyCode, KeyEvent event) { 464 if (event.getAction() == KeyEvent.ACTION_DOWN) { 465 switch (keyCode) { 466 case KeyEvent.KEYCODE_DPAD_UP: 467 case KeyEvent.KEYCODE_DPAD_DOWN: 468 { 469 if (mCancelAnimation) { 470 mScrollAnimatorSet.end(); 471 } 472 if (!mScrollAnimatorSet.isRunning()) { 473 mCancelAnimation = false; 474 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 475 mNextValue = 476 adjustValueInValidRange( 477 mCurrentValue + 1); 478 startScrollAnimation(true); 479 } else { 480 mNextValue = 481 adjustValueInValidRange( 482 mCurrentValue - 1); 483 startScrollAnimation(false); 484 } 485 } 486 return true; 487 } 488 } 489 } else if (event.getAction() == KeyEvent.ACTION_UP) { 490 switch (keyCode) { 491 case KeyEvent.KEYCODE_DPAD_UP: 492 case KeyEvent.KEYCODE_DPAD_DOWN: 493 { 494 mCancelAnimation = true; 495 return true; 496 } 497 } 498 } 499 return false; 500 } 501 }); 502 mNumberViewHolder.setScrollY(mNumberViewHeight); 503 504 mFocusGainAnimator = new AnimatorSet(); 505 mFocusGainAnimator.playTogether( 506 ObjectAnimator.ofFloat( 507 mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1], 508 "alpha", 509 0f, 510 sAlphaForAdjacentNumber), 511 ObjectAnimator.ofFloat( 512 mNumberViews[CURRENT_NUMBER_VIEW_INDEX], 513 "alpha", 514 sAlphaForFocusedNumber, 515 0f), 516 ObjectAnimator.ofFloat( 517 mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1], 518 "alpha", 519 0f, 520 sAlphaForAdjacentNumber), 521 ObjectAnimator.ofFloat(mBackgroundView, "alpha", 0f, 1f)); 522 mFocusGainAnimator.setDuration( 523 context.getResources().getInteger(android.R.integer.config_shortAnimTime)); 524 mFocusGainAnimator.addListener( 525 new AnimatorListenerAdapter() { 526 @Override 527 public void onAnimationEnd(Animator animator) { 528 mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText( 529 mBackgroundView.getText()); 530 mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha( 531 sAlphaForFocusedNumber); 532 mBackgroundView.setText(""); 533 } 534 }); 535 536 mFocusLossAnimator = new AnimatorSet(); 537 mFocusLossAnimator.playTogether( 538 ObjectAnimator.ofFloat( 539 mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1], 540 "alpha", 541 sAlphaForAdjacentNumber, 542 0f), 543 ObjectAnimator.ofFloat( 544 mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1], 545 "alpha", 546 sAlphaForAdjacentNumber, 547 0f), 548 ObjectAnimator.ofFloat(mBackgroundView, "alpha", 1f, 0f)); 549 mFocusLossAnimator.setDuration( 550 context.getResources().getInteger(android.R.integer.config_shortAnimTime)); 551 552 mScrollAnimatorSet = new AnimatorSet(); 553 mScrollAnimatorSet.setDuration( 554 context.getResources().getInteger(R.integer.pin_number_scroll_duration)); 555 mScrollAnimatorSet.addListener( 556 new AnimatorListenerAdapter() { 557 @Override 558 public void onAnimationEnd(Animator animation) { 559 // Set mCurrent value when scroll animation is finished. 560 mCurrentValue = mNextValue; 561 updateText(); 562 mNumberViewHolder.setScrollY(mNumberViewHeight); 563 mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha( 564 sAlphaForAdjacentNumber); 565 mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha( 566 sAlphaForFocusedNumber); 567 mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha( 568 sAlphaForAdjacentNumber); 569 } 570 }); 571 } 572 573 static void loadResources(Context context) { 574 if (sFocusedNumberEnterAnimator == null) { 575 TypedValue outValue = new TypedValue(); 576 context.getResources() 577 .getValue(R.dimen.pin_alpha_for_focused_number, outValue, true); 578 sAlphaForFocusedNumber = outValue.getFloat(); 579 context.getResources() 580 .getValue(R.dimen.pin_alpha_for_adjacent_number, outValue, true); 581 sAlphaForAdjacentNumber = outValue.getFloat(); 582 583 sFocusedNumberEnterAnimator = 584 AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_enter); 585 sFocusedNumberExitAnimator = 586 AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_exit); 587 sAdjacentNumberEnterAnimator = 588 AnimatorInflater.loadAnimator( 589 context, R.animator.pin_adjacent_number_enter); 590 sAdjacentNumberExitAnimator = 591 AnimatorInflater.loadAnimator(context, R.animator.pin_adjacent_number_exit); 592 } 593 } 594 595 @Override 596 public boolean dispatchKeyEvent(KeyEvent event) { 597 if (event.getAction() == KeyEvent.ACTION_UP) { 598 int keyCode = event.getKeyCode(); 599 if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { 600 mNextValue = adjustValueInValidRange(keyCode - KeyEvent.KEYCODE_0); 601 updateFocus(false); 602 } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER 603 || keyCode == KeyEvent.KEYCODE_ENTER) { 604 if (mNextNumberPicker == null) { 605 String pin = mDialog.getPinInput(); 606 if (!TextUtils.isEmpty(pin)) { 607 mDialog.done(pin); 608 } 609 } else { 610 mNextNumberPicker.requestFocus(); 611 } 612 return true; 613 } 614 } 615 return super.dispatchKeyEvent(event); 616 } 617 618 void startScrollAnimation(boolean scrollUp) { 619 mFocusGainAnimator.end(); 620 mFocusLossAnimator.end(); 621 final ValueAnimator scrollAnimator = 622 ValueAnimator.ofInt(0, scrollUp ? mNumberViewHeight : -mNumberViewHeight); 623 scrollAnimator.addUpdateListener( 624 new ValueAnimator.AnimatorUpdateListener() { 625 @Override 626 public void onAnimationUpdate(ValueAnimator animation) { 627 int value = (Integer) animation.getAnimatedValue(); 628 mNumberViewHolder.setScrollY(value + mNumberViewHeight); 629 } 630 }); 631 scrollAnimator.setDuration( 632 getResources().getInteger(R.integer.pin_number_scroll_duration)); 633 634 if (scrollUp) { 635 sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]); 636 sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]); 637 sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]); 638 sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 2]); 639 } else { 640 sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 2]); 641 sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]); 642 sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]); 643 sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]); 644 } 645 646 mScrollAnimatorSet.playTogether( 647 scrollAnimator, 648 sAdjacentNumberExitAnimator, 649 sFocusedNumberExitAnimator, 650 sFocusedNumberEnterAnimator, 651 sAdjacentNumberEnterAnimator); 652 mScrollAnimatorSet.start(); 653 } 654 655 void setValueRangeAndResetText(int min, int max) { 656 if (min > max) { 657 throw new IllegalArgumentException( 658 "The min value should be greater than or equal to the max value"); 659 } else if (min == NOT_INITIALIZED) { 660 throw new IllegalArgumentException( 661 "The min value should be greater than Integer.MIN_VALUE."); 662 } 663 mMinValue = min; 664 mMaxValue = max; 665 mNextValue = mCurrentValue = NOT_INITIALIZED; 666 for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { 667 mNumberViews[i].setText(i == CURRENT_NUMBER_VIEW_INDEX ? INITIAL_TEXT : ""); 668 } 669 mBackgroundView.setText(INITIAL_TEXT); 670 } 671 672 void setPinDialogFragment(PinDialogFragment dlg) { 673 mDialog = dlg; 674 } 675 676 void setNextNumberPicker(PinNumberPicker picker) { 677 mNextNumberPicker = picker; 678 } 679 680 int getValue() { 681 if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) { 682 throw new IllegalStateException("Value is not set"); 683 } 684 return mCurrentValue; 685 } 686 687 void updateFocus(boolean withAnimation) { 688 mScrollAnimatorSet.end(); 689 mFocusGainAnimator.end(); 690 mFocusLossAnimator.end(); 691 updateText(); 692 if (mNumberViewHolder.isFocused()) { 693 if (withAnimation) { 694 mBackgroundView.setText(String.valueOf(mCurrentValue)); 695 mFocusGainAnimator.start(); 696 } else { 697 mBackgroundView.setAlpha(1f); 698 mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(sAlphaForAdjacentNumber); 699 mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(sAlphaForAdjacentNumber); 700 } 701 } else { 702 if (withAnimation) { 703 mFocusLossAnimator.start(); 704 } else { 705 mBackgroundView.setAlpha(0f); 706 mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(0f); 707 mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(0f); 708 } 709 mNumberViewHolder.setScrollY(mNumberViewHeight); 710 } 711 } 712 713 private void updateText() { 714 boolean wasNotInitialized = false; 715 if (mNumberViewHolder.isFocused() && mCurrentValue == NOT_INITIALIZED) { 716 mNextValue = mCurrentValue = mMinValue; 717 wasNotInitialized = true; 718 } 719 if (mCurrentValue >= mMinValue && mCurrentValue <= mMaxValue) { 720 for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { 721 if (wasNotInitialized && i == CURRENT_NUMBER_VIEW_INDEX) { 722 // In order to show the text change animation, keep the text of 723 // mNumberViews[CURRENT_NUMBER_VIEW_INDEX]. 724 } else { 725 mNumberViews[i].setText( 726 String.valueOf( 727 adjustValueInValidRange( 728 mCurrentValue - CURRENT_NUMBER_VIEW_INDEX + i))); 729 } 730 } 731 } 732 } 733 734 private int adjustValueInValidRange(int value) { 735 int interval = mMaxValue - mMinValue + 1; 736 if (value < mMinValue - interval || value > mMaxValue + interval) { 737 throw new IllegalArgumentException( 738 "The value( " + value + ") is too small or too big to adjust"); 739 } 740 return (value < mMinValue) 741 ? value + interval 742 : (value > mMaxValue) ? value - interval : value; 743 } 744 } 745 746 /** 747 * A listener to the result of {@link PinDialogFragment}. Any activity requiring pin code 748 * checking should implement this listener to receive the result. 749 */ 750 public interface OnPinCheckedListener { 751 /** 752 * Called when {@link PinDialogFragment} is dismissed. 753 * 754 * @param checked {@code true} if the pin code entered is checked to be correct, otherwise 755 * {@code false}. 756 * @param type The dialog type regarding to what pin entering is for. 757 * @param rating The target rating to unblock for. 758 */ 759 void onPinChecked(boolean checked, int type, String rating); 760 } 761 } 762