Home | History | Annotate | Download | only in radio
      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