• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
canAppendCharacterToNumber(@onNull String character, @NonNull String number)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          */
isValidChannel(@onNull String number)84         boolean isValidChannel(@NonNull String number);
85 
86         /**
87          * Returns an integer representation of the given number in hertz.
88          */
convertToHz(@onNull String number)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          */
shouldAppendPeriod(@onNull String number)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          */
onBack()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          */
onDone(ProgramSelector sel)113         void onDone(ProgramSelector sel);
114     }
115 
ManualTunerController(Context context, View container, int currentRadioBand)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      */
initializeChannelButtons(View container)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      */
updateCurrentRadioBand(int band)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      */
initializeManualTunerButtons(View container)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      */
setDoneButtonListener(ManualTunerClickListener listener)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      */
updateButtonState()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
canAppendCharacterToNumber(@onNull String character, @NonNull String number)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
isValidChannel(@onNull String number)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
convertToHz(@onNull String number)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
shouldAppendPeriod(@onNull String number)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
canAppendCharacterToNumber(@onNull String character, @NonNull String number)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
isValidChannel(@onNull String number)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
convertToHz(@onNull String number)481         public int convertToHz(@NonNull String number) {
482             return Integer.valueOf(convertNumberToKilohertz(number));
483         }
484 
485         @Override
shouldAppendPeriod(@onNull String number)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          */
convertNumberToKilohertz(String number)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      */
setChannelText()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      */
resetChannel()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
onClick(View v)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          */
deleteLastCharacterIfPeriod()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 
TuneButtonClickListener(String value)580         TuneButtonClickListener(String value) {
581             mValue = value;
582         }
583 
584         @Override
onClick(View v)585         public void onClick(View v) {
586             mCurrentChannel.append(mValue);
587             setChannelText();
588             updateButtonState();
589         }
590     }
591 }
592