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