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