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