1 /* 2 * Copyright (c) 2016, 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 package com.android.car.radio; 17 18 import android.content.Context; 19 import android.hardware.radio.RadioManager; 20 import android.support.annotation.NonNull; 21 import android.view.View; 22 import android.widget.Button; 23 import android.widget.TextView; 24 import com.android.car.radio.service.RadioStation; 25 26 import java.util.ArrayList; 27 import java.util.List; 28 29 /** 30 * A controller for the various buttons in the manual tuner screen. 31 */ 32 public class ManualTunerController { 33 /** 34 * The total number of controllable buttons in the manual tuner. This value represents the 35 * values 0 - 9. 36 */ 37 private static final int NUM_OF_MANUAL_TUNER_BUTTONS = 10; 38 39 private final StringBuffer mCurrentChannel = new StringBuffer(); 40 41 private final Context mContext; 42 private final TextView mChannelView; 43 private final RadioBandButton mAmBandButton; 44 private final RadioBandButton mFmBandButton; 45 46 private int mCurrentRadioBand; 47 48 private final List<Button> mManualTunerButtons = new ArrayList<>(NUM_OF_MANUAL_TUNER_BUTTONS); 49 50 private final String mNumberZero; 51 private final String mNumberOne; 52 private final String mNumberTwo; 53 private final String mNumberThree; 54 private final String mNumberFour; 55 private final String mNumberFive; 56 private final String mNumberSix; 57 private final String mNumberSeven; 58 private final String mNumberEight; 59 private final String mNumberNine; 60 private final String mPeriod; 61 62 private ChannelValidator mChannelValidator; 63 private final ChannelValidator mAmChannelValidator = new AmChannelValidator(); 64 private final ChannelValidator mFmChannelValidator = new FMChannelValidator(); 65 66 private final int mEnabledButtonColor; 67 private final int mDisabledButtonColor; 68 69 private final View mDoneButton; 70 private ManualTunerClickListener mManualTunerClickListener; 71 72 /** 73 * An interface that will perform various validations on {@link #mCurrentChannel}. 74 */ 75 public interface ChannelValidator { 76 /** 77 * Returns {@code true} if the given character is allowed to be appended to the given 78 * number. 79 */ canAppendCharacterToNumber(@onNull String character, @NonNull String number)80 boolean canAppendCharacterToNumber(@NonNull String character, @NonNull String number); 81 82 /** 83 * Returns {@code true} if the given number if a valid radio channel frequency. 84 */ isValidChannel(@onNull String number)85 boolean isValidChannel(@NonNull String number); 86 87 /** 88 * Returns an integer representation of the given number in hertz. 89 */ convertToHz(@onNull String number)90 int convertToHz(@NonNull String number); 91 92 /** 93 * Returns {@code true} if a period (decimal point) should be appended to the given 94 * number. For example, FM channels should automatically add a period if the given number 95 * is over 100 or has two digits. 96 */ shouldAppendPeriod(@onNull String number)97 boolean shouldAppendPeriod(@NonNull String number); 98 } 99 100 /** 101 * An interface for a class that will be notified when the done or back buttons of the manual 102 * tuner has been clicked. 103 */ 104 public interface ManualTunerClickListener { 105 /** 106 * Called when the back button on the manual tuner has been clicked. 107 */ onBack()108 void onBack(); 109 110 /** 111 * Called when the done button has been clicked with the given station that the user has 112 * selected. 113 */ onDone(RadioStation station)114 void onDone(RadioStation station); 115 } 116 ManualTunerController(Context context, View container, int currentRadioBand)117 public ManualTunerController(Context context, View container, int currentRadioBand) { 118 mContext = context; 119 mChannelView = (TextView) container.findViewById(R.id.manual_tuner_channel); 120 121 // Default to FM band. 122 if (currentRadioBand != RadioManager.BAND_FM && currentRadioBand != RadioManager.BAND_AM) { 123 currentRadioBand = RadioManager.BAND_FM; 124 } 125 126 mCurrentRadioBand = currentRadioBand; 127 128 mChannelValidator = mCurrentRadioBand == RadioManager.BAND_AM 129 ? mAmChannelValidator 130 : mFmChannelValidator; 131 132 mEnabledButtonColor = mContext.getColor(R.color.manual_tuner_button_text); 133 mDisabledButtonColor = mContext.getColor(R.color.car_radio_control_button_disabled); 134 135 View backButton = container.findViewById(R.id.exit_manual_tuner_button); 136 backButton.setOnClickListener(new View.OnClickListener() { 137 @Override 138 public void onClick(View v) { 139 if (mManualTunerClickListener != null) { 140 mManualTunerClickListener.onBack(); 141 } 142 } 143 }); 144 145 mDoneButton = container.findViewById(R.id.manual_tuner_done_button); 146 mDoneButton.setOnClickListener(mDoneButtonClickListener); 147 148 mNumberZero = context.getString(R.string.manual_tuner_0); 149 mNumberOne = context.getString(R.string.manual_tuner_1); 150 mNumberTwo = context.getString(R.string.manual_tuner_2); 151 mNumberThree = context.getString(R.string.manual_tuner_3); 152 mNumberFour = context.getString(R.string.manual_tuner_4); 153 mNumberFive = context.getString(R.string.manual_tuner_5); 154 mNumberSix = context.getString(R.string.manual_tuner_6); 155 mNumberSeven = context.getString(R.string.manual_tuner_7); 156 mNumberEight = context.getString(R.string.manual_tuner_8); 157 mNumberNine = context.getString(R.string.manual_tuner_9); 158 mPeriod = context.getString(R.string.manual_tuner_period); 159 160 initializeManualTunerButtons(container); 161 162 mAmBandButton = (RadioBandButton) container.findViewById(R.id.manual_tuner_am_band); 163 mAmBandButton.setOnClickListener(mBandSwitcherListener); 164 mFmBandButton = (RadioBandButton) container.findViewById(R.id.manual_tuner_fm_band); 165 mFmBandButton.setOnClickListener(mBandSwitcherListener); 166 167 if (mCurrentRadioBand == RadioManager.BAND_AM) { 168 mAmBandButton.setIsBandSelected(true); 169 } else { 170 mFmBandButton.setIsBandSelected(true); 171 } 172 173 updateButtonState(); 174 } 175 176 /** 177 * Sets up the click listeners and tags for the manual tuner buttons. 178 */ initializeManualTunerButtons(View container)179 private void initializeManualTunerButtons(View container) { 180 Button numberZero = (Button) container.findViewById(R.id.manual_tuner_0); 181 numberZero.setOnClickListener(new TuneButtonClickListener(mNumberZero)); 182 numberZero.setTag(R.id.manual_tuner_button_value, mNumberZero); 183 mManualTunerButtons.add(numberZero); 184 185 Button numberOne = (Button) container.findViewById(R.id.manual_tuner_1); 186 numberOne.setOnClickListener(new TuneButtonClickListener(mNumberOne)); 187 numberOne.setTag(R.id.manual_tuner_button_value, mNumberOne); 188 mManualTunerButtons.add(numberOne); 189 190 Button numberTwo = (Button) container.findViewById(R.id.manual_tuner_2); 191 numberTwo.setOnClickListener(new TuneButtonClickListener(mNumberTwo)); 192 numberTwo.setTag(R.id.manual_tuner_button_value, mNumberTwo); 193 mManualTunerButtons.add(numberTwo); 194 195 Button numberThree = (Button) container.findViewById(R.id.manual_tuner_3); 196 numberThree.setOnClickListener(new TuneButtonClickListener(mNumberThree)); 197 numberThree.setTag(R.id.manual_tuner_button_value, mNumberThree); 198 mManualTunerButtons.add(numberThree); 199 200 Button numberFour = (Button) container.findViewById(R.id.manual_tuner_4); 201 numberFour.setOnClickListener(new TuneButtonClickListener(mNumberFour)); 202 numberFour.setTag(R.id.manual_tuner_button_value, mNumberFour); 203 mManualTunerButtons.add(numberFour); 204 205 Button numberFive = (Button) container.findViewById(R.id.manual_tuner_5); 206 numberFive.setOnClickListener(new TuneButtonClickListener(mNumberFive)); 207 numberFive.setTag(R.id.manual_tuner_button_value, mNumberFive); 208 mManualTunerButtons.add(numberFive); 209 210 Button numberSix = (Button) container.findViewById(R.id.manual_tuner_6); 211 numberSix.setOnClickListener(new TuneButtonClickListener(mNumberSix)); 212 numberSix.setTag(R.id.manual_tuner_button_value, mNumberSix); 213 mManualTunerButtons.add(numberSix); 214 215 Button numberSeven = (Button) container.findViewById(R.id.manual_tuner_7); 216 numberSeven.setOnClickListener(new TuneButtonClickListener(mNumberSeven)); 217 numberSeven.setTag(R.id.manual_tuner_button_value, mNumberSeven); 218 mManualTunerButtons.add(numberSeven); 219 220 Button numberEight = (Button) container.findViewById(R.id.manual_tuner_8); 221 numberEight.setOnClickListener(new TuneButtonClickListener(mNumberEight)); 222 numberEight.setTag(R.id.manual_tuner_button_value, mNumberEight); 223 mManualTunerButtons.add(numberEight); 224 225 Button numberNine = (Button) container.findViewById(R.id.manual_tuner_9); 226 numberNine.setOnClickListener(new TuneButtonClickListener(mNumberNine)); 227 numberNine.setTag(R.id.manual_tuner_button_value, mNumberNine); 228 mManualTunerButtons.add(numberNine); 229 230 container.findViewById(R.id.manual_tuner_backspace).setOnClickListener(mBackspaceListener); 231 } 232 233 /** 234 * Sets the given {@link ManualTunerClickListener} to be notified when the done button of the manual 235 * tuner has been clicked. 236 */ setDoneButtonListener(ManualTunerClickListener listener)237 public void setDoneButtonListener(ManualTunerClickListener listener) { 238 mManualTunerClickListener = listener; 239 } 240 241 /** 242 * Iterates through all the buttons in {@link #mManualTunerButtons} and updates whether or not 243 * they are enabled based on the current {@link #mChannelValidator}. 244 */ updateButtonState()245 private void updateButtonState() { 246 String currentChannel = mCurrentChannel.toString(); 247 248 for (int i = 0, size = mManualTunerButtons.size(); i < size; i++) { 249 Button button = mManualTunerButtons.get(i); 250 String value = (String) button.getTag(R.id.manual_tuner_button_value); 251 252 boolean enabled = mChannelValidator.canAppendCharacterToNumber(value, currentChannel); 253 254 button.setEnabled(enabled); 255 button.setTextColor(enabled ? mEnabledButtonColor : mDisabledButtonColor); 256 } 257 258 mDoneButton.setEnabled(mChannelValidator.isValidChannel(currentChannel)); 259 } 260 261 /** 262 * A {@link ChannelValidator} for the AM band. Note this validator is for US regions. 263 */ 264 private final class AmChannelValidator implements ChannelValidator { 265 private static final int AM_LOWER_LIMIT = 530; 266 private static final int AM_UPPER_LIMIT = 1700; 267 268 @Override canAppendCharacterToNumber(@onNull String character, @NonNull String number)269 public boolean canAppendCharacterToNumber(@NonNull String character, 270 @NonNull String number) { 271 // There are no decimal points for AM numbers. 272 if (character.equals(mPeriod)) { 273 return false; 274 } 275 276 int charValue = Integer.valueOf(character); 277 278 switch (number.length()) { 279 case 0: 280 // 5 and 1 are the first digits of AM_LOWER_LIMIT and AM_UPPER_LIMIT. 281 return charValue >= 5 || charValue == 1; 282 case 1: 283 // Ensure that the number is above the lower AM limit of 530. 284 if (number.equals(mNumberFive)) { 285 return charValue >= 3; 286 } 287 288 return true; 289 case 2: 290 // Any number is allowed to be appended if the current AM station being entered 291 // is a number in the 1000s. 292 if (String.valueOf(number.charAt(0)).equals(mNumberOne)) { 293 return true; 294 } 295 296 // Otherwise, only zero is allowed because AM stations go in increments of 10. 297 return character.equals(mNumberZero); 298 case 3: 299 // AM station are in increments of 10, so for a 3 digit AM station, only a 300 // zero is allowed at the end. Note, no need to check if the "number" is a 301 // number in the 1000s because this should be handled by "case 2". 302 return character.equals(mNumberZero); 303 default: 304 // Otherwise, just disallow the character. 305 return false; 306 } 307 } 308 309 @Override isValidChannel(@onNull String number)310 public boolean isValidChannel(@NonNull String number) { 311 if (number.length() == 0) { 312 return false; 313 } 314 315 // No decimal points for AM channels. 316 if (number.contains(mPeriod)) { 317 return false; 318 } 319 320 int value = Integer.valueOf(number); 321 return value >= AM_LOWER_LIMIT && value <= AM_UPPER_LIMIT; 322 } 323 324 @Override convertToHz(@onNull String number)325 public int convertToHz(@NonNull String number) { 326 // The number should already been in Hz, so just perform a straight conversion. 327 return Integer.valueOf(number); 328 } 329 330 @Override shouldAppendPeriod(@onNull String number)331 public boolean shouldAppendPeriod(@NonNull String number) { 332 // No decimal points for AM channels. 333 return false; 334 } 335 } 336 337 /** 338 * A {@link ChannelValidator} for the FM band. Note that this validator is for US regions. 339 */ 340 private final class FMChannelValidator implements ChannelValidator { 341 private static final int FM_LOWER_LIMIT = 87900; 342 private static final int FM_UPPER_LIMIT = 107900; 343 344 /** 345 * The value including the decimal point of the FM upper limit. 346 */ 347 private static final String FM_UPPER_LIMIT_CHARACTERISTIC = "107."; 348 349 /** 350 * The lower limit of FM channels in kilohertz before the decimal point. 351 */ 352 private static final int FM_LOWER_LIMIT_NO_DECIMAL_KHZ = 87; 353 354 private static final String KILOHERTZ_CONVERSION_DIGITS = "000"; 355 private static final String KILOHERTZ_CONVERSION_DIGITS_WITH_DECIMAL = "00"; 356 357 @Override canAppendCharacterToNumber(@onNull String character, @NonNull String number)358 public boolean canAppendCharacterToNumber(@NonNull String character, 359 @NonNull String number) { 360 int indexOfPeriod = number.indexOf(mPeriod); 361 362 if (character.equals(mPeriod)) { 363 // Only one decimal point is allowed. 364 if (indexOfPeriod != -1) { 365 return false; 366 } 367 368 // There needs to be at least two digits before a decimal point is allowed. 369 return number.length() >= 2; 370 } 371 372 if (number.length() == 0) { 373 // No need to check for the decimal point here because it's handled by the first 374 // if case. 375 int charValue = Integer.valueOf(character); 376 377 // 8 and 1 are the first digits of FM_LOWER_LIMIT and FM_UPPER_LIMIT; 378 return charValue >= 8 || charValue == 1; 379 } 380 381 if (indexOfPeriod == -1) { 382 switch (number.length()) { 383 case 1: 384 // If the number is 1, then only a zero is allowed afterwards since FM 385 // channels can only go up to 108.1. 386 if (number.equals(mNumberOne)) { 387 return character.equals(mNumberZero); 388 } 389 390 // If the number 8, then we need to only allow 7 and above. This is because 391 // the lower limit of FM channels is 87.9. 392 if (number.equals(mNumberEight)) { 393 int numberValue = Integer.valueOf(character); 394 return numberValue >= 7; 395 } 396 397 // Otherwise, any number is allowed. 398 return true; 399 400 case 2: 401 // If there are two digits, only allow another character to be added if the 402 // resulting character will be in the 100s but less than 107. 403 return String.valueOf(number.charAt(0)).equals(mNumberOne) 404 && !character.equals(mNumberEight) 405 && !character.equals(mNumberNine); 406 407 case 3: 408 default: 409 // If there are already three digits, no more numbers can be added 410 // without a decimal point. 411 return false; 412 } 413 } else if (number.length() - 1 > indexOfPeriod) { 414 // Only one number if allowed after the decimal point. 415 return false; 416 } 417 418 // If the number being entered it right up on the FM upper limit, then the allowed 419 // character can only be a 1 because the upper limit is 108.1. 420 if (number.equals(FM_UPPER_LIMIT_CHARACTERISTIC)) { 421 return character.equals(mNumberNine); 422 } 423 424 // Otherwise, FM frequencies can only end in an odd digit (e.g. 96.5 and not 96.4). 425 int charValue = Integer.valueOf(character); 426 return charValue % 2 == 1; 427 } 428 429 @Override isValidChannel(@onNull String number)430 public boolean isValidChannel(@NonNull String number) { 431 if (number.length() == 0) { 432 return false; 433 } 434 435 // Strip the period from the number and ensure the number string is represented in 436 // kilohertz. 437 String updatedNumber = convertNumberToKilohertz(number); 438 int value = Integer.valueOf(updatedNumber); 439 return value >= FM_LOWER_LIMIT && value <= FM_UPPER_LIMIT; 440 } 441 442 @Override convertToHz(@onNull String number)443 public int convertToHz(@NonNull String number) { 444 return Integer.valueOf(convertNumberToKilohertz(number)); 445 } 446 447 @Override shouldAppendPeriod(@onNull String number)448 public boolean shouldAppendPeriod(@NonNull String number) { 449 // Check if there is already a decimal point. 450 if (number.contains(mPeriod)) { 451 return false; 452 } 453 454 int value = Integer.valueOf(number); 455 return value >= FM_LOWER_LIMIT_NO_DECIMAL_KHZ; 456 } 457 458 /** 459 * Converts the given number to its kilohertz representation. For example, 87.9 will be 460 * converted to 87900. 461 */ convertNumberToKilohertz(String number)462 private String convertNumberToKilohertz(String number) { 463 if (number.contains(mPeriod)) { 464 return number.replace(mPeriod, "") 465 + KILOHERTZ_CONVERSION_DIGITS_WITH_DECIMAL; 466 } 467 468 return number + KILOHERTZ_CONVERSION_DIGITS; 469 } 470 } 471 472 /** 473 * Sets the {@link #mCurrentChannel} on {@link #mChannelView}. Will append a decimal point to 474 * the text if necessary. This is based on the current {@link ChannelValidator}. 475 */ setChannelText()476 private void setChannelText() { 477 if (mChannelValidator.shouldAppendPeriod(mCurrentChannel.toString())) { 478 mCurrentChannel.append(mPeriod); 479 } 480 481 mChannelView.setText(mCurrentChannel.toString()); 482 } 483 484 private final View.OnClickListener mDoneButtonClickListener = new View.OnClickListener() { 485 @Override 486 public void onClick(View v) { 487 if (mManualTunerClickListener == null) { 488 return; 489 } 490 491 int channelFrequency = mChannelValidator.convertToHz(mCurrentChannel.toString()); 492 RadioStation station = new RadioStation(channelFrequency, 0 /* subChannelNumber */, 493 mCurrentRadioBand, null /* rds */); 494 495 mManualTunerClickListener.onDone(station); 496 } 497 }; 498 499 private final View.OnClickListener mBandSwitcherListener = new View.OnClickListener() { 500 @Override 501 public void onClick(View v) { 502 // Currently only AM and FM bands are supported. 503 if (v == mAmBandButton) { 504 mCurrentRadioBand = RadioManager.BAND_AM; 505 mChannelValidator = mAmChannelValidator; 506 mAmBandButton.setIsBandSelected(true); 507 mFmBandButton.setIsBandSelected(false); 508 } else { 509 mCurrentRadioBand = RadioManager.BAND_FM; 510 mChannelValidator = mFmChannelValidator; 511 mAmBandButton.setIsBandSelected(false); 512 mFmBandButton.setIsBandSelected(true); 513 } 514 515 mChannelView.setText(null); 516 517 // Clear the string buffer by setting the length to zero rather than allocating a new 518 // one. 519 mCurrentChannel.setLength(0); 520 521 updateButtonState(); 522 } 523 }; 524 525 private final View.OnClickListener mBackspaceListener = new View.OnClickListener() { 526 @Override 527 public void onClick(View v) { 528 if (mCurrentChannel.length() == 0) { 529 return; 530 } 531 532 // Since the period cannot be added manually by the user, remove it for them. Both 533 // before and after the deletion of a non-period character. 534 deleteLastCharacterIfPeriod(); 535 mCurrentChannel.deleteCharAt(mCurrentChannel.length() - 1); 536 537 mChannelView.setText(mCurrentChannel.toString()); 538 539 updateButtonState(); 540 } 541 542 /** 543 * Checks if the last character in {@link ManualTunerController#mCurrentChannel} is a 544 * period. If it is, then removes it. 545 */ 546 private void deleteLastCharacterIfPeriod() { 547 int lastIndex = mCurrentChannel.length() - 1; 548 String lastCharacter = String.valueOf(mCurrentChannel.charAt(lastIndex)); 549 550 // If we delete a character and the resulting last character is the decimal point, 551 // delete that as well. 552 if (lastCharacter.equals(mPeriod)) { 553 mCurrentChannel.deleteCharAt(lastIndex); 554 } 555 } 556 }; 557 558 /** 559 * A {@link android.view.View.OnClickListener} for each of the manual tuner buttons that 560 * will update the number being displayed when pressed. 561 */ 562 private class TuneButtonClickListener implements View.OnClickListener { 563 private final String mValue; 564 TuneButtonClickListener(String value)565 public TuneButtonClickListener(String value) { 566 mValue = value; 567 } 568 569 @Override onClick(View v)570 public void onClick(View v) { 571 mCurrentChannel.append(mValue); 572 setChannelText(); 573 updateButtonState(); 574 } 575 } 576 } 577