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