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