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