blob: 1ca5171778c6b287cb991e5fc519dc2818fcadb5 [file] [log] [blame]
/*
* Copyright (c) 2016, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.radio;
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
import java.util.ArrayList;
import java.util.List;
/**
* A controller for the various buttons in the manual tuner screen.
*/
public class ManualTunerController {
/**
* The total number of controllable buttons in the manual tuner. This value represents the
* values 0 - 9.
*/
private static final int NUM_OF_MANUAL_TUNER_BUTTONS = 10;
private final StringBuilder mCurrentChannel = new StringBuilder();
private final TextView mChannelView;
private int mCurrentRadioBand;
private final List<Button> mManualTunerButtons = new ArrayList<>(NUM_OF_MANUAL_TUNER_BUTTONS);
private final String mNumberZero;
private final String mNumberOne;
private final String mNumberTwo;
private final String mNumberThree;
private final String mNumberFour;
private final String mNumberFive;
private final String mNumberSix;
private final String mNumberSeven;
private final String mNumberEight;
private final String mNumberNine;
private final String mPeriod;
private ChannelValidator mChannelValidator;
private final ChannelValidator mAmChannelValidator = new AmChannelValidator();
private final ChannelValidator mFmChannelValidator = new FMChannelValidator();
private final int mEnabledButtonColor;
private final int mDisabledButtonColor;
private View mDoneButton;
private ManualTunerClickListener mManualTunerClickListener;
/**
* An interface that will perform various validations on {@link #mCurrentChannel}.
*/
public interface ChannelValidator {
/**
* Returns {@code true} if the given character is allowed to be appended to the given
* number.
*/
boolean canAppendCharacterToNumber(@NonNull String character, @NonNull String number);
/**
* Returns {@code true} if the given number if a valid radio channel frequency.
*/
boolean isValidChannel(@NonNull String number);
/**
* Returns an integer representation of the given number in hertz.
*/
int convertToHz(@NonNull String number);
/**
* Returns {@code true} if a period (decimal point) should be appended to the given
* number. For example, FM channels should automatically add a period if the given number
* is over 100 or has two digits.
*/
boolean shouldAppendPeriod(@NonNull String number);
}
/**
* An interface for a class that will be notified when the done or back buttons of the manual
* tuner has been clicked.
*/
public interface ManualTunerClickListener {
/**
* Called when the back button on the manual tuner has been clicked.
*/
void onBack();
/**
* Called when the done button has been clicked with the given station that the user has
* selected.
*/
void onDone(ProgramSelector sel);
}
public ManualTunerController(Context context, View container, int currentRadioBand) {
mChannelView = container.findViewById(R.id.manual_tuner_channel);
// Default to FM band.
if (currentRadioBand != RadioManager.BAND_FM && currentRadioBand != RadioManager.BAND_AM) {
currentRadioBand = RadioManager.BAND_FM;
}
mCurrentRadioBand = currentRadioBand;
mChannelValidator = mCurrentRadioBand == RadioManager.BAND_AM
? mAmChannelValidator
: mFmChannelValidator;
mEnabledButtonColor = context.getColor(R.color.manual_tuner_button_text);
mDisabledButtonColor = context.getColor(R.color.car_radio_control_button_disabled);
mNumberZero = context.getString(R.string.manual_tuner_0);
mNumberOne = context.getString(R.string.manual_tuner_1);
mNumberTwo = context.getString(R.string.manual_tuner_2);
mNumberThree = context.getString(R.string.manual_tuner_3);
mNumberFour = context.getString(R.string.manual_tuner_4);
mNumberFive = context.getString(R.string.manual_tuner_5);
mNumberSix = context.getString(R.string.manual_tuner_6);
mNumberSeven = context.getString(R.string.manual_tuner_7);
mNumberEight = context.getString(R.string.manual_tuner_8);
mNumberNine = context.getString(R.string.manual_tuner_9);
mPeriod = context.getString(R.string.manual_tuner_period);
initializeChannelButtons(container);
initializeManualTunerButtons(container);
updateButtonState();
}
/**
* Initializes the buttons responsible for adjusting the channel to be entered by the manual
* tuner.
*/
private void initializeChannelButtons(View container) {
RadioBandButton amBandButton = container.findViewById(R.id.manual_tuner_am_band);
RadioBandButton fmBandButton = container.findViewById(R.id.manual_tuner_fm_band);
mDoneButton = container.findViewById(R.id.manual_tuner_done_button);
View backButton = container.findViewById(R.id.exit_manual_tuner_button);
if (backButton != null) {
backButton.setOnClickListener(v -> {
if (mManualTunerClickListener != null) {
mManualTunerClickListener.onBack();
}
});
}
mDoneButton.setOnClickListener(v -> {
if (mManualTunerClickListener == null) {
return;
}
int channelFrequency = mChannelValidator.convertToHz(mCurrentChannel.toString());
mManualTunerClickListener.onDone(
ProgramSelectorExt.createAmFmSelector(channelFrequency));
});
if (amBandButton != null) {
amBandButton.setOnClickListener(v -> {
mCurrentRadioBand = RadioManager.BAND_AM;
mChannelValidator = mAmChannelValidator;
amBandButton.setIsBandSelected(true);
fmBandButton.setIsBandSelected(false);
resetChannel();
});
}
if (fmBandButton != null) {
fmBandButton.setOnClickListener(v -> {
mCurrentRadioBand = RadioManager.BAND_FM;
mChannelValidator = mFmChannelValidator;
amBandButton.setIsBandSelected(false);
fmBandButton.setIsBandSelected(true);
resetChannel();
});
}
if (mCurrentRadioBand == RadioManager.BAND_AM && amBandButton != null) {
amBandButton.setIsBandSelected(true);
} else if (fmBandButton != null) {
fmBandButton.setIsBandSelected(true);
}
}
/**
* Refreshes tuner key state with new radio band, if changed without using AM/FM band buttons
*/
public void updateCurrentRadioBand(int band) {
mCurrentRadioBand = band;
if (band == RadioManager.BAND_FM) {
mChannelValidator = mFmChannelValidator;
} else {
mChannelValidator = mAmChannelValidator;
}
resetChannel();
}
/**
* Sets up the click listeners and tags for the manual tuner buttons.
*/
private void initializeManualTunerButtons(View container) {
Button numberZero = container.findViewById(R.id.manual_tuner_0);
numberZero.setOnClickListener(new TuneButtonClickListener(mNumberZero));
numberZero.setTag(R.id.manual_tuner_button_value, mNumberZero);
mManualTunerButtons.add(numberZero);
Button numberOne = container.findViewById(R.id.manual_tuner_1);
numberOne.setOnClickListener(new TuneButtonClickListener(mNumberOne));
numberOne.setTag(R.id.manual_tuner_button_value, mNumberOne);
mManualTunerButtons.add(numberOne);
Button numberTwo = container.findViewById(R.id.manual_tuner_2);
numberTwo.setOnClickListener(new TuneButtonClickListener(mNumberTwo));
numberTwo.setTag(R.id.manual_tuner_button_value, mNumberTwo);
mManualTunerButtons.add(numberTwo);
Button numberThree = container.findViewById(R.id.manual_tuner_3);
numberThree.setOnClickListener(new TuneButtonClickListener(mNumberThree));
numberThree.setTag(R.id.manual_tuner_button_value, mNumberThree);
mManualTunerButtons.add(numberThree);
Button numberFour = container.findViewById(R.id.manual_tuner_4);
numberFour.setOnClickListener(new TuneButtonClickListener(mNumberFour));
numberFour.setTag(R.id.manual_tuner_button_value, mNumberFour);
mManualTunerButtons.add(numberFour);
Button numberFive = container.findViewById(R.id.manual_tuner_5);
numberFive.setOnClickListener(new TuneButtonClickListener(mNumberFive));
numberFive.setTag(R.id.manual_tuner_button_value, mNumberFive);
mManualTunerButtons.add(numberFive);
Button numberSix = container.findViewById(R.id.manual_tuner_6);
numberSix.setOnClickListener(new TuneButtonClickListener(mNumberSix));
numberSix.setTag(R.id.manual_tuner_button_value, mNumberSix);
mManualTunerButtons.add(numberSix);
Button numberSeven = container.findViewById(R.id.manual_tuner_7);
numberSeven.setOnClickListener(new TuneButtonClickListener(mNumberSeven));
numberSeven.setTag(R.id.manual_tuner_button_value, mNumberSeven);
mManualTunerButtons.add(numberSeven);
Button numberEight = container.findViewById(R.id.manual_tuner_8);
numberEight.setOnClickListener(new TuneButtonClickListener(mNumberEight));
numberEight.setTag(R.id.manual_tuner_button_value, mNumberEight);
mManualTunerButtons.add(numberEight);
Button numberNine = container.findViewById(R.id.manual_tuner_9);
numberNine.setOnClickListener(new TuneButtonClickListener(mNumberNine));
numberNine.setTag(R.id.manual_tuner_button_value, mNumberNine);
mManualTunerButtons.add(numberNine);
container.findViewById(R.id.manual_tuner_backspace)
.setOnClickListener(new BackSpaceListener());
}
/**
* Sets the given {@link ManualTunerClickListener} to be notified when the done button of the manual
* tuner has been clicked.
*/
public void setDoneButtonListener(ManualTunerClickListener listener) {
mManualTunerClickListener = listener;
}
/**
* Iterates through all the buttons in {@link #mManualTunerButtons} and updates whether or not
* they are enabled based on the current {@link #mChannelValidator}.
*/
private void updateButtonState() {
String currentChannel = mCurrentChannel.toString();
for (int i = 0, size = mManualTunerButtons.size(); i < size; i++) {
Button button = mManualTunerButtons.get(i);
String value = (String) button.getTag(R.id.manual_tuner_button_value);
boolean enabled = mChannelValidator.canAppendCharacterToNumber(value, currentChannel);
button.setEnabled(enabled);
button.setTextColor(enabled ? mEnabledButtonColor : mDisabledButtonColor);
}
mDoneButton.setEnabled(mChannelValidator.isValidChannel(currentChannel));
}
/**
* A {@link ChannelValidator} for the AM band. Note this validator is for US regions.
*/
private final class AmChannelValidator implements ChannelValidator {
private static final int AM_LOWER_LIMIT = 530;
private static final int AM_UPPER_LIMIT = 1700;
@Override
public boolean canAppendCharacterToNumber(@NonNull String character,
@NonNull String number) {
// There are no decimal points for AM numbers.
if (character.equals(mPeriod)) {
return false;
}
int charValue = Integer.valueOf(character);
switch (number.length()) {
case 0:
// 5 and 1 are the first digits of AM_LOWER_LIMIT and AM_UPPER_LIMIT.
return charValue >= 5 || charValue == 1;
case 1:
// Ensure that the number is above the lower AM limit of 530.
return !number.equals(mNumberFive) || charValue >= 3;
case 2:
// Any number is allowed to be appended if the current AM station being entered
// is a number in the 1000s.
if (String.valueOf(number.charAt(0)).equals(mNumberOne)) {
return true;
}
// Otherwise, only zero is allowed because AM stations go in increments of 10.
return character.equals(mNumberZero);
case 3:
// AM station are in increments of 10, so for a 3 digit AM station, only a
// zero is allowed at the end. Note, no need to check if the "number" is a
// number in the 1000s because this should be handled by "case 2".
return character.equals(mNumberZero);
default:
// Otherwise, just disallow the character.
return false;
}
}
@Override
public boolean isValidChannel(@NonNull String number) {
if (number.length() == 0) {
return false;
}
// No decimal points for AM channels.
if (number.contains(mPeriod)) {
return false;
}
int value = Integer.valueOf(number);
return value >= AM_LOWER_LIMIT && value <= AM_UPPER_LIMIT;
}
@Override
public int convertToHz(@NonNull String number) {
// The number should already been in Hz, so just perform a straight conversion.
return Integer.valueOf(number);
}
@Override
public boolean shouldAppendPeriod(@NonNull String number) {
// No decimal points for AM channels.
return false;
}
}
/**
* A {@link ChannelValidator} for the FM band. Note that this validator is for US regions.
*/
private final class FMChannelValidator implements ChannelValidator {
private static final int FM_LOWER_LIMIT = 87900;
private static final int FM_UPPER_LIMIT = 107900;
/**
* The value including the decimal point of the FM upper limit.
*/
private static final String FM_UPPER_LIMIT_CHARACTERISTIC = "107.";
/**
* The lower limit of FM channels in kilohertz before the decimal point.
*/
private static final int FM_LOWER_LIMIT_NO_DECIMAL_KHZ = 87;
private static final String KILOHERTZ_CONVERSION_DIGITS = "000";
private static final String KILOHERTZ_CONVERSION_DIGITS_WITH_DECIMAL = "00";
@Override
public boolean canAppendCharacterToNumber(@NonNull String character,
@NonNull String number) {
int indexOfPeriod = number.indexOf(mPeriod);
if (character.equals(mPeriod)) {
// Only one decimal point is allowed.
if (indexOfPeriod != -1) {
return false;
}
// There needs to be at least two digits before a decimal point is allowed.
return number.length() >= 2;
}
if (number.length() == 0) {
// No need to check for the decimal point here because it's handled by the first
// if case.
int charValue = Integer.valueOf(character);
// 8 and 1 are the first digits of FM_LOWER_LIMIT and FM_UPPER_LIMIT;
return charValue >= 8 || charValue == 1;
}
if (indexOfPeriod == -1) {
switch (number.length()) {
case 1:
// If the number is 1, then only a zero is allowed afterwards since FM
// channels can only go up to 108.1.
if (number.equals(mNumberOne)) {
return character.equals(mNumberZero);
}
// If the number 8, then we need to only allow 7 and above. This is because
// the lower limit of FM channels is 87.9.
if (number.equals(mNumberEight)) {
int numberValue = Integer.valueOf(character);
return numberValue >= 7;
}
// Otherwise, any number is allowed.
return true;
case 2:
// If there are two digits, only allow another character to be added if the
// resulting character will be in the 100s but less than 107.
return String.valueOf(number.charAt(0)).equals(mNumberOne)
&& !character.equals(mNumberEight)
&& !character.equals(mNumberNine);
case 3:
default:
// If there are already three digits, no more numbers can be added
// without a decimal point.
return false;
}
} else if (number.length() - 1 > indexOfPeriod) {
// Only one number if allowed after the decimal point.
return false;
}
// If the number being entered it right up on the FM upper limit, then the allowed
// character can only be a 1 because the upper limit is 108.1.
if (number.equals(FM_UPPER_LIMIT_CHARACTERISTIC)) {
return character.equals(mNumberNine);
}
// Otherwise, FM frequencies can only end in an odd digit (e.g. 96.5 and not 96.4).
int charValue = Integer.valueOf(character);
return charValue % 2 == 1;
}
@Override
public boolean isValidChannel(@NonNull String number) {
if (number.length() == 0) {
return false;
}
// Strip the period from the number and ensure the number string is represented in
// kilohertz.
String updatedNumber = convertNumberToKilohertz(number);
int value = Integer.valueOf(updatedNumber);
return value >= FM_LOWER_LIMIT && value <= FM_UPPER_LIMIT;
}
@Override
public int convertToHz(@NonNull String number) {
return Integer.valueOf(convertNumberToKilohertz(number));
}
@Override
public boolean shouldAppendPeriod(@NonNull String number) {
// Check if there is already a decimal point.
if (number.contains(mPeriod)) {
return false;
}
int value = Integer.valueOf(number);
return value >= FM_LOWER_LIMIT_NO_DECIMAL_KHZ;
}
/**
* Converts the given number to its kilohertz representation. For example, 87.9 will be
* converted to 87900.
*/
private String convertNumberToKilohertz(String number) {
if (number.contains(mPeriod)) {
return number.replace(mPeriod, "")
+ KILOHERTZ_CONVERSION_DIGITS_WITH_DECIMAL;
}
return number + KILOHERTZ_CONVERSION_DIGITS;
}
}
/**
* Sets the {@link #mCurrentChannel} on {@link #mChannelView}. Will append a decimal point to
* the text if necessary. This is based on the current {@link ChannelValidator}.
*/
private void setChannelText() {
if (mChannelValidator.shouldAppendPeriod(mCurrentChannel.toString())) {
mCurrentChannel.append(mPeriod);
}
mChannelView.setText(mCurrentChannel.toString());
}
/**
* Resets any radio station that may have been entered and updates the button states
* accordingly.
*/
private void resetChannel() {
mChannelView.setText(null);
// Clear the string buffer by setting the length to zero rather than allocating a new
// one.
mCurrentChannel.setLength(0);
updateButtonState();
}
/**
* A {@link android.view.View.OnClickListener} that handles back space clicks. It is responsible
* for removing characters from the {@link #mChannelView} TextView.
*/
private class BackSpaceListener implements View.OnClickListener {
@Override
public void onClick(View v) {
if (mCurrentChannel.length() == 0) {
return;
}
// Since the period cannot be added manually by the user, remove it for them. Both
// before and after the deletion of a non-period character.
deleteLastCharacterIfPeriod();
mCurrentChannel.deleteCharAt(mCurrentChannel.length() - 1);
mChannelView.setText(mCurrentChannel.toString());
updateButtonState();
}
/**
* Checks if the last character in {@link ManualTunerController#mCurrentChannel} is a
* period. If it is, then removes it.
*/
private void deleteLastCharacterIfPeriod() {
int lastIndex = mCurrentChannel.length() - 1;
String lastCharacter = String.valueOf(mCurrentChannel.charAt(lastIndex));
// If we delete a character and the resulting last character is the decimal point,
// delete that as well.
if (lastCharacter.equals(mPeriod)) {
mCurrentChannel.deleteCharAt(lastIndex);
}
}
}
/**
* A {@link android.view.View.OnClickListener} for each of the manual tuner buttons that
* will update the number being displayed when pressed.
*/
private class TuneButtonClickListener implements View.OnClickListener {
private final String mValue;
TuneButtonClickListener(String value) {
mValue = value;
}
@Override
public void onClick(View v) {
mCurrentChannel.append(mValue);
setChannelText();
updateButtonState();
}
}
}