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 
17 package com.android.car.radio;
18 
19 import android.animation.ArgbEvaluator;
20 import android.animation.ValueAnimator;
21 import android.app.Activity;
22 import android.app.LoaderManager;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.Loader;
27 import android.content.ServiceConnection;
28 import android.graphics.Color;
29 import android.hardware.radio.RadioManager;
30 import android.hardware.radio.RadioTuner;
31 import android.media.AudioManager;
32 import android.os.Bundle;
33 import android.os.IBinder;
34 import android.os.RemoteException;
35 import android.support.annotation.ColorInt;
36 import android.support.annotation.Nullable;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.view.View;
40 import com.android.car.radio.service.IRadioCallback;
41 import com.android.car.radio.service.IRadioManager;
42 import com.android.car.radio.service.RadioRds;
43 import com.android.car.radio.service.RadioStation;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 
48 /**
49  * A controller that handles the display of metadata on the current radio station.
50  */
51 public class RadioController implements
52         RadioStorage.PresetsChangeListener,
53         RadioStorage.PreScannedChannelChangeListener,
54         LoaderManager.LoaderCallbacks<List<RadioStation>> {
55     private static final String TAG = "Em.RadioController";
56     private static final int CHANNEL_LOADER_ID = 0;
57 
58     /**
59      * The percentage by which to darken the color that should be set on the status bar.
60      * This darkening gives the status bar the illusion that it is transparent.
61      *
62      * @see {@link RadioController#setShouldColorStatusBar(boolean)}
63      */
64     private static final float STATUS_BAR_DARKEN_PERCENTAGE = 0.4f;
65 
66     /**
67      * The animation time for when the background of the radio shifts to a different color.
68      */
69     private static final int BACKGROUND_CHANGE_ANIM_TIME_MS = 450;
70     private static final int INVALID_BACKGROUND_COLOR = 0;
71 
72     private final int CHANNEL_CHANGE_DURATION_MS = 200;
73 
74     private int mCurrentChannelNumber = RadioStorage.INVALID_RADIO_CHANNEL;
75 
76     private final Activity mActivity;
77     private IRadioManager mRadioManager;
78 
79     private View mRadioBackground;
80     private boolean mShouldColorStatusBar;
81 
82     /**
83      * An additional layer on top of the background that should match the color of
84      * {@link #mRadioBackground}. This view should only exist in the preset list. The reason this
85      * layer cannot be transparent is because it needs to be elevated, and elevation does not
86      * work if the background is undefined or transparent.
87      */
88     private View mRadioPresetBackground;
89 
90     private View mRadioErrorDisplay;
91 
92     private final RadioChannelColorMapper mColorMapper;
93     @ColorInt private int mCurrentBackgroundColor = INVALID_BACKGROUND_COLOR;
94 
95     private PrescannedRadioStationAdapter mAdapter;
96     private PreScannedChannelLoader mChannelLoader;
97 
98     private final RadioDisplayController mRadioDisplayController;
99     private boolean mHasDualTuners;
100 
101     /**
102      * Keeps track of if the user has manually muted the radio. This value is used to determine
103      * whether or not to un-mute the radio after an {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT}
104      * event has been received.
105      */
106     private boolean mUserHasMuted;
107 
108     private final RadioStorage mRadioStorage;
109 
110     /**
111      * The current radio band. This value is one of the BAND_* values from {@link RadioManager}.
112      * For example, {@link RadioManager#BAND_FM}.
113      */
114     private int mCurrentRadioBand = RadioStorage.INVALID_RADIO_BAND;
115     private final String mAmBandString;
116     private final String mFmBandString;
117 
118     private RadioRds mCurrentRds;
119 
120     private RadioStationChangeListener mStationChangeListener;
121 
122     /**
123      * Interface for a class that will be notified when the current radio station has been changed.
124      */
125     public interface RadioStationChangeListener {
126         /**
127          * Called when the current radio station has changed in the radio.
128          *
129          * @param station The current radio station.
130          */
onRadioStationChanged(RadioStation station)131         void onRadioStationChanged(RadioStation station);
132     }
133 
RadioController(Activity activity)134     public RadioController(Activity activity) {
135         mActivity = activity;
136 
137         mRadioDisplayController = new RadioDisplayController(mActivity);
138         mColorMapper = RadioChannelColorMapper.getInstance(mActivity);
139 
140         mAmBandString = mActivity.getString(R.string.radio_am_text);
141         mFmBandString = mActivity.getString(R.string.radio_fm_text);
142 
143         mRadioStorage = RadioStorage.getInstance(mActivity);
144         mRadioStorage.addPresetsChangeListener(this);
145     }
146 
147     /**
148      * Initializes this {@link RadioController} to control the UI whose root is the given container.
149      */
initialize(View container)150     public void initialize(View container) {
151         mCurrentBackgroundColor = INVALID_BACKGROUND_COLOR;
152 
153         mRadioDisplayController.initialize(container);
154 
155         mRadioDisplayController.setBackwardSeekButtonListener(mBackwardSeekClickListener);
156         mRadioDisplayController.setForwardSeekButtonListener(mForwardSeekClickListener);
157         mRadioDisplayController.setPlayButtonListener(mPlayPauseClickListener);
158         mRadioDisplayController.setAddPresetButtonListener(mPresetButtonClickListener);
159 
160         mRadioBackground = container;
161         mRadioPresetBackground = container.findViewById(R.id.preset_current_card_container);
162 
163         mRadioErrorDisplay = container.findViewById(R.id.radio_error_display);
164 
165         updateRadioDisplay();
166     }
167 
168     /**
169      * Set whether or not this controller should also update the color of the status bar to match
170      * the current background color of the radio. The color that will be set on the status bar
171      * will be slightly darker, giving the illusion that the status bar is transparent.
172      *
173      * <p>This method is needed because of scene transitions. Scene transitions do not take into
174      * account padding that is added programmatically. Since there is no way to get the height of
175      * the status bar and set it in XML, it needs to be done in code. This breaks the scene
176      * transition.
177      *
178      * <p>To make this work, the status bar is not actually translucent; it is colored to appear
179      * that way via this method.
180      */
setShouldColorStatusBar(boolean shouldColorStatusBar)181     public void setShouldColorStatusBar(boolean shouldColorStatusBar) {
182        mShouldColorStatusBar = shouldColorStatusBar;
183     }
184 
185     /**
186      * Sets the listener that will be notified whenever the radio station changes.
187      */
setRadioStationChangeListener(RadioStationChangeListener listener)188     public void setRadioStationChangeListener(RadioStationChangeListener listener) {
189         mStationChangeListener = listener;
190     }
191 
192     /**
193      * Starts the controller to handle radio tuning. This method should be called to begin
194      * radio playback.
195      */
start()196     public void start() {
197         if (Log.isLoggable(TAG, Log.DEBUG)) {
198             Log.d(TAG, "starting radio");
199         }
200 
201         Intent bindIntent = new Intent(mActivity, RadioService.class);
202         if (!mActivity.bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) {
203             Log.e(TAG, "Failed to connect to RadioService.");
204         }
205 
206         updateRadioDisplay();
207     }
208 
209     /**
210      * Retrieves information about the current radio station from {@link #mRadioManager} and updates
211      * the display of that information accordingly.
212      */
updateRadioDisplay()213     private void updateRadioDisplay() {
214         if (mRadioManager == null) {
215             return;
216         }
217 
218         try {
219             RadioStation station = mRadioManager.getCurrentRadioStation();
220 
221             if (Log.isLoggable(TAG, Log.DEBUG)) {
222                 Log.d(TAG, "updateRadioDisplay(); current station: " + station);
223             }
224 
225             mHasDualTuners = mRadioManager.hasDualTuners();
226 
227             if (mHasDualTuners) {
228                 initializeDualTunerController();
229             } else {
230                 mRadioDisplayController.setSingleChannelDisplay(mRadioBackground);
231             }
232 
233             // Update the AM/FM band display.
234             mCurrentRadioBand = station.getRadioBand();
235             updateAmFmDisplayState();
236 
237             // Update the channel number.
238             setRadioChannel(station.getChannelNumber());
239 
240             // Ensure the play button properly reflects the current mute state.
241             mRadioDisplayController.setPlayPauseButtonState(mRadioManager.isMuted());
242 
243             mCallback.onRadioMetadataChanged(station.getRds());
244 
245             if (mStationChangeListener != null) {
246                 mStationChangeListener.onRadioStationChanged(station);
247             }
248         } catch (RemoteException e) {
249             Log.e(TAG, "updateRadioDisplay(); remote exception: " + e.getMessage());
250         }
251     }
252 
253     /**
254      * Tunes the radio to the given channel if it is valid and a {@link RadioTuner} has been opened.
255      */
tuneToRadioChannel(RadioStation radioStation)256     public void tuneToRadioChannel(RadioStation radioStation) {
257         if (mRadioManager == null) {
258             return;
259         }
260 
261         try {
262             mRadioManager.tune(radioStation);
263         } catch (RemoteException e) {
264             Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage());
265         }
266     }
267 
268     /**
269      * Returns the band this radio is currently tuned to.
270      */
getCurrentRadioBand()271     public int getCurrentRadioBand() {
272         return mCurrentRadioBand;
273     }
274 
275     /**
276      * Returns the radio station that is currently playing on the radio. If this controller is
277      * not connected to the {@link RadioService} or a radio station cannot be retrieved, then
278      * {@code null} is returned.
279      */
280     @Nullable
getCurrentRadioStation()281     public RadioStation getCurrentRadioStation() {
282         if (mRadioManager == null) {
283             return null;
284         }
285 
286         try {
287             return mRadioManager.getCurrentRadioStation();
288         } catch (RemoteException e) {
289             Log.e(TAG, "getCurrentRadioStation(); error retrieving current station: "
290                     + e.getMessage());
291         }
292 
293         return null;
294     }
295 
296     /**
297      * Opens the given current radio band. Currently, this only supports FM and AM bands.
298      *
299      * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
300      *                  {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
301      */
openRadioBand(int radioBand)302     public void openRadioBand(int radioBand) {
303         if (mRadioManager == null || radioBand == mCurrentRadioBand) {
304             return;
305         }
306 
307         // Reset the channel number so that we do not animate number changes between band changes.
308         mCurrentChannelNumber = RadioStorage.INVALID_RADIO_CHANNEL;
309 
310         setCurrentRadioBand(radioBand);
311         mRadioStorage.storeRadioBand(mCurrentRadioBand);
312 
313         try {
314             mRadioManager.openRadioBand(radioBand);
315 
316             updateAmFmDisplayState();
317 
318             // Sets the initial mute state. This will resolve the mute state should be if an
319             // {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT} event is received followed by an
320             // {@link AudioManager#AUDIOFOCUS_GAIN} event. In this case, the radio will un-mute itself
321             // if the user has not muted beforehand.
322             if (mUserHasMuted) {
323                 mRadioManager.mute();
324             }
325 
326             // Ensure the play button properly reflects the current mute state.
327             mRadioDisplayController.setPlayPauseButtonState(mRadioManager.isMuted());
328 
329             maybeTuneToStoredRadioChannel();
330         } catch (RemoteException e) {
331             Log.e(TAG, "openRadioBand(); remote exception: " + e.getMessage());
332         }
333     }
334 
335     /**
336      * Attempts to tune to the last played radio channel for a particular band. For example, if
337      * the user switches to the AM band from FM, this method will attempt to tune to the last
338      * AM band that the user was on.
339      *
340      * <p>If a stored radio station cannot be found, then this method will initiate a seek so that
341      * the radio is always on a valid radio station.
342      */
maybeTuneToStoredRadioChannel()343     private void maybeTuneToStoredRadioChannel() {
344         mCurrentChannelNumber = mRadioStorage.getStoredRadioChannel(mCurrentRadioBand);
345 
346         if (Log.isLoggable(TAG, Log.DEBUG)) {
347             Log.d(TAG, String.format("maybeTuneToStoredRadioChannel(); band: %s, channel %s",
348                     mCurrentRadioBand, mCurrentChannelNumber));
349         }
350 
351         // Tune to a stored radio channel if it exists.
352         if (mCurrentChannelNumber != RadioStorage.INVALID_RADIO_CHANNEL) {
353             RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */,
354                     mCurrentRadioBand, mCurrentRds);
355             tuneToRadioChannel(station);
356         } else {
357             // Otherwise, ensure that the radio is on a valid radio station (i.e. it will not
358             // start playing static) by initiating a seek.
359             try {
360                 mRadioManager.seekForward();
361             } catch (RemoteException e) {
362                 Log.e(TAG, "maybeTuneToStoredRadioChannel(); remote exception: " + e.getMessage());
363             }
364         }
365     }
366 
367     /**
368      * Delegates to the {@link RadioDisplayController} to highlight the radio band that matches
369      * up to {@link #mCurrentRadioBand}.
370      */
updateAmFmDisplayState()371     private void updateAmFmDisplayState() {
372         switch (mCurrentRadioBand) {
373             case RadioManager.BAND_FM:
374                 mRadioDisplayController.setChannelBand(mFmBandString);
375                 break;
376 
377             case RadioManager.BAND_AM:
378                 mRadioDisplayController.setChannelBand(mAmBandString);
379                 break;
380 
381             // TODO: Support BAND_FM_HD and BAND_AM_HD.
382 
383             default:
384                 mRadioDisplayController.setChannelBand(null);
385         }
386     }
387 
388     /**
389      * Sets the radio channel to display.
390      * @param channel The radio channel frequency in Hz.
391      */
setRadioChannel(int channel)392     private void setRadioChannel(int channel) {
393         if (Log.isLoggable(TAG, Log.DEBUG)) {
394             Log.d(TAG, "Setting radio channel: " + channel);
395         }
396 
397         if (channel <= 0) {
398             mCurrentChannelNumber = channel;
399             mRadioDisplayController.setChannelNumber("");
400             return;
401         }
402 
403         if (mHasDualTuners) {
404             int position = mAdapter.getIndexOrInsertForStation(channel, mCurrentRadioBand);
405             mRadioDisplayController.setCurrentStationInList(position);
406         }
407 
408         switch (mCurrentRadioBand) {
409             case RadioManager.BAND_FM:
410                 setRadioChannelForFm(channel);
411                 break;
412 
413             case RadioManager.BAND_AM:
414                 setRadioChannelForAm(channel);
415                 break;
416 
417             // TODO: Support BAND_FM_HD and BAND_AM_HD.
418 
419             default:
420                 // Do nothing and don't check presets, so return here.
421                 return;
422         }
423 
424         mCurrentChannelNumber = channel;
425 
426         mRadioDisplayController.setChannelIsPreset(
427                 mRadioStorage.isPreset(channel, mCurrentRadioBand));
428 
429         mRadioStorage.storeRadioChannel(mCurrentRadioBand, mCurrentChannelNumber);
430 
431         maybeUpdateBackgroundColor();
432     }
433 
setRadioChannelForAm(int channel)434     private void setRadioChannelForAm(int channel) {
435         // No need for animation if radio channel has never been set.
436         if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) {
437             mRadioDisplayController.setChannelNumber(
438                     RadioChannelFormatter.AM_FORMATTER.format(channel));
439             return;
440         }
441 
442         animateRadioChannelChange(mCurrentChannelNumber, channel, mAmAnimatorListener);
443     }
444 
setRadioChannelForFm(int channel)445     private void setRadioChannelForFm(int channel) {
446         // FM channels are displayed in Khz. e.g. 88500 is displayed as 88.5.
447         float channelInKHz = (float) channel / 1000;
448 
449         // No need for animation if radio channel has never been set.
450         if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) {
451             mRadioDisplayController.setChannelNumber(
452                     RadioChannelFormatter.FM_FORMATTER.format(channelInKHz));
453             return;
454         }
455 
456         float startChannelNumber = (float) mCurrentChannelNumber / 1000;
457         animateRadioChannelChange(startChannelNumber, channelInKHz, mFmAnimatorListener);
458     }
459 
460     /**
461      * Checks if the color of the radio background should be changed, and if so, animates that
462      * color change.
463      */
maybeUpdateBackgroundColor()464     private void maybeUpdateBackgroundColor() {
465         if (mRadioBackground == null) {
466             return;
467         }
468 
469         int newColor = mColorMapper.getColorForStation(mCurrentRadioBand, mCurrentChannelNumber);
470 
471         // No animation required if the colors are the same.
472         if (newColor == mCurrentBackgroundColor) {
473             return;
474         }
475 
476         // If the current background color is invalid, then just set as the new color without any
477         // animation.
478         if (mCurrentBackgroundColor == INVALID_BACKGROUND_COLOR) {
479             mCurrentBackgroundColor = newColor;
480             setBackgroundColor(newColor);
481         }
482 
483         // Otherwise, animate the background color change.
484         ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
485                 mCurrentBackgroundColor, newColor);
486         colorAnimation.setDuration(BACKGROUND_CHANGE_ANIM_TIME_MS);
487         colorAnimation.addUpdateListener(mBackgroundColorUpdater);
488         colorAnimation.start();
489 
490         mCurrentBackgroundColor = newColor;
491     }
492 
setBackgroundColor(int backgroundColor)493     private void setBackgroundColor(int backgroundColor) {
494         mRadioBackground.setBackgroundColor(backgroundColor);
495 
496         if (mRadioPresetBackground != null) {
497             mRadioPresetBackground.setBackgroundColor(backgroundColor);
498         }
499 
500         if (mShouldColorStatusBar) {
501             int red = darkenColor(Color.red(backgroundColor));
502             int green = darkenColor(Color.green(backgroundColor));
503             int blue = darkenColor(Color.blue(backgroundColor));
504             int alpha = Color.alpha(backgroundColor);
505 
506             mActivity.getWindow().setStatusBarColor(
507                     Color.argb(alpha, red, green, blue));
508         }
509     }
510 
511     /**
512      * Darkens the given color by {@link #STATUS_BAR_DARKEN_PERCENTAGE}.
513      */
darkenColor(int color)514     private int darkenColor(int color) {
515         return (int) Math.max(color - (color * STATUS_BAR_DARKEN_PERCENTAGE), 0);
516     }
517 
518     /**
519      * Animates the text in channel number from the given starting value to the given
520      * end value.
521      */
animateRadioChannelChange(float startValue, float endValue, ValueAnimator.AnimatorUpdateListener listener)522     private void animateRadioChannelChange(float startValue, float endValue,
523             ValueAnimator.AnimatorUpdateListener listener) {
524         ValueAnimator animator = new ValueAnimator();
525         animator.setObjectValues(startValue, endValue);
526         animator.setDuration(CHANNEL_CHANGE_DURATION_MS);
527         animator.addUpdateListener(listener);
528         animator.start();
529     }
530 
531     /**
532      * Clears all metadata including song title, artist and station information.
533      */
clearMetadataDisplay()534     private void clearMetadataDisplay() {
535         mCurrentRds = null;
536 
537         mRadioDisplayController.setCurrentSongArtistOrStation(null);
538         mRadioDisplayController.setCurrentSongTitle(null);
539     }
540 
541     /**
542      * Sets the internal {@link #mCurrentRadioBand} to be the given radio band. Will also take care
543      * of restarting a load of the pre-scanned radio stations for the given band if there are dual
544      * tuners on the device.
545      */
setCurrentRadioBand(int radioBand)546     private void setCurrentRadioBand(int radioBand) {
547         if (mCurrentRadioBand == radioBand) {
548             return;
549         }
550 
551         mCurrentRadioBand = radioBand;
552 
553         if (mChannelLoader != null) {
554             mAdapter.setStations(new ArrayList<>());
555             mChannelLoader.setCurrentRadioBand(radioBand);
556             mChannelLoader.forceLoad();
557         }
558     }
559 
560     /**
561      * Closes any active {@link RadioTuner}s and releases audio focus.
562      */
close()563     private void close() {
564         if (Log.isLoggable(TAG, Log.DEBUG)) {
565             Log.d(TAG, "close()");
566         }
567 
568         // Lost focus, so display that the radio is not playing anymore.
569         mRadioDisplayController.setPlayPauseButtonState(true);
570     }
571 
572     /**
573      * Closes all active connections in the {@link RadioController}.
574      */
shutdown()575     public void shutdown() {
576         if (Log.isLoggable(TAG, Log.DEBUG)) {
577             Log.d(TAG, "shutdown()");
578         }
579 
580         mActivity.unbindService(mServiceConnection);
581         mRadioStorage.removePresetsChangeListener(this);
582         mRadioStorage.removePreScannedChannelChangeListener(this);
583 
584         if (mRadioManager != null) {
585             try {
586                 mRadioManager.removeRadioTunerCallback(mCallback);
587             } catch (RemoteException e) {
588                 Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage());
589             }
590         }
591 
592         close();
593     }
594 
595     /**
596      * Initializes all the extra components that are needed if this radio has dual tuners.
597      */
initializeDualTunerController()598     private void initializeDualTunerController() {
599         if (Log.isLoggable(TAG, Log.DEBUG)) {
600             Log.d(TAG, "initializeDualTunerController()");
601         }
602 
603         mRadioStorage.addPreScannedChannelChangeListener(RadioController.this);
604 
605         if (mAdapter == null) {
606             mAdapter = new PrescannedRadioStationAdapter();
607         }
608 
609         mRadioDisplayController.setChannelListDisplay(mRadioBackground, mAdapter);
610 
611         // Initialize the loader that will load the pre-scanned channels for the current band.
612         mActivity.getLoaderManager().initLoader(CHANNEL_LOADER_ID, null /* args */,
613                 RadioController.this /* callback */).forceLoad();
614     }
615 
616     @Override
onPresetsRefreshed()617     public void onPresetsRefreshed() {
618         // Check if the current channel's preset status has changed.
619         mRadioDisplayController.setChannelIsPreset(
620                 mRadioStorage.isPreset(mCurrentChannelNumber, mCurrentRadioBand));
621     }
622 
623     @Override
onPreScannedChannelChange(int radioBand)624     public void onPreScannedChannelChange(int radioBand) {
625         // If pre-scanned channels have changed for the current radio band, then refresh the list
626         // that is currently being displayed.
627         if (radioBand == mCurrentRadioBand && mChannelLoader != null) {
628             mChannelLoader.forceLoad();
629         }
630     }
631 
632     @Override
onCreateLoader(int id, Bundle args)633     public Loader<List<RadioStation>> onCreateLoader(int id, Bundle args) {
634         // Only one loader, so no need to check for id.
635         mChannelLoader = new PreScannedChannelLoader(mActivity /* context */);
636         mChannelLoader.setCurrentRadioBand(mCurrentRadioBand);
637 
638         return mChannelLoader;
639     }
640 
641     @Override
onLoadFinished(Loader<List<RadioStation>> loader, List<RadioStation> preScannedStations)642     public void onLoadFinished(Loader<List<RadioStation>> loader,
643             List<RadioStation> preScannedStations) {
644         if (Log.isLoggable(TAG, Log.DEBUG)) {
645             int size = preScannedStations == null ? 0 : preScannedStations.size();
646             Log.d(TAG, "onLoadFinished(); number of pre-scanned stations: " + size);
647         }
648 
649         if (Log.isLoggable(TAG, Log.VERBOSE)) {
650             for (RadioStation station : preScannedStations) {
651                 Log.v(TAG, "station: " + station.toString());
652             }
653         }
654 
655         mAdapter.setStations(preScannedStations);
656 
657         int position = mAdapter.setStartingStation(mCurrentChannelNumber, mCurrentRadioBand);
658         mRadioDisplayController.setCurrentStationInList(position);
659     }
660 
661     @Override
onLoaderReset(Loader<List<RadioStation>> loader)662     public void onLoaderReset(Loader<List<RadioStation>> loader) {}
663 
664     /**
665      * Value animator for AM values.
666      */
667     private ValueAnimator.AnimatorUpdateListener mAmAnimatorListener =
668             new ValueAnimator.AnimatorUpdateListener() {
669                 public void onAnimationUpdate(ValueAnimator animation) {
670                     mRadioDisplayController.setChannelNumber(
671                             RadioChannelFormatter.AM_FORMATTER.format(
672                                     animation.getAnimatedValue()));
673                 }
674             };
675 
676     /**
677      * Value animator for FM values.
678      */
679     private ValueAnimator.AnimatorUpdateListener mFmAnimatorListener =
680             new ValueAnimator.AnimatorUpdateListener() {
681                 public void onAnimationUpdate(ValueAnimator animation) {
682                     mRadioDisplayController.setChannelNumber(
683                             RadioChannelFormatter.FM_FORMATTER.format(
684                                     animation.getAnimatedValue()));
685                 }
686             };
687 
688     private final IRadioCallback.Stub mCallback = new IRadioCallback.Stub() {
689         @Override
690         public void onRadioStationChanged(RadioStation station) {
691             if (Log.isLoggable(TAG, Log.DEBUG)) {
692                 Log.d(TAG, "onRadioStationChanged: " + station);
693             }
694 
695             if (station == null) {
696                 return;
697             }
698 
699             if (mCurrentChannelNumber != station.getChannelNumber()) {
700                 setRadioChannel(station.getChannelNumber());
701             }
702 
703             onRadioMetadataChanged(station.getRds());
704 
705             // Notify that the current radio station has changed.
706             if (mStationChangeListener != null) {
707                 try {
708                     mStationChangeListener.onRadioStationChanged(
709                             mRadioManager.getCurrentRadioStation());
710                 } catch (RemoteException e) {
711                     Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage());
712                 }
713             }
714         }
715 
716         /**
717          * Updates radio information based on the given {@link RadioRds}.
718          */
719         @Override
720         public void onRadioMetadataChanged(RadioRds radioRds) {
721             if (Log.isLoggable(TAG, Log.DEBUG)) {
722                 Log.d(TAG, "onMetadataChanged(); metadata: " + radioRds);
723             }
724 
725             clearMetadataDisplay();
726 
727             if (radioRds == null) {
728                 return;
729             }
730 
731             mCurrentRds = radioRds;
732 
733             if (Log.isLoggable(TAG, Log.DEBUG)) {
734                 Log.d(TAG, "mCurrentRds: " + mCurrentRds);
735             }
736 
737             String programService = radioRds.getProgramService();
738             String artistMetadata = radioRds.getSongArtist();
739 
740             mRadioDisplayController.setCurrentSongArtistOrStation(
741                     TextUtils.isEmpty(artistMetadata) ? programService : artistMetadata);
742             mRadioDisplayController.setCurrentSongTitle(radioRds.getSongTitle());
743 
744             // Since new metadata exists, update the preset that is stored in the database if
745             // it exists.
746             if (TextUtils.isEmpty(programService)) {
747                 return;
748             }
749 
750             RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */,
751                     mCurrentRadioBand, radioRds);
752             boolean isPreset = mRadioStorage.isPreset(station);
753 
754             if (isPreset) {
755                 if (Log.isLoggable(TAG, Log.DEBUG)) {
756                     Log.d(TAG, "Current channel is a preset; updating metadata in the database.");
757                 }
758 
759                 mRadioStorage.storePreset(station);
760             }
761         }
762 
763         @Override
764         public void onRadioBandChanged(int radioBand) {
765             if (Log.isLoggable(TAG, Log.DEBUG)) {
766                 Log.d(TAG, "onRadioBandChanged: " + radioBand);
767             }
768 
769             setCurrentRadioBand(radioBand);
770             updateAmFmDisplayState();
771 
772             // Check that the radio channel is being correctly formatted.
773             setRadioChannel(mCurrentChannelNumber);
774         }
775 
776         @Override
777         public void onRadioMuteChanged(boolean isMuted) {
778             mRadioDisplayController.setPlayPauseButtonState(isMuted);
779         }
780 
781         @Override
782         public void onError(int status) {
783             Log.e(TAG, "Radio callback error with status: " + status);
784             close();
785         }
786     };
787 
788     private final View.OnClickListener mBackwardSeekClickListener = new View.OnClickListener() {
789         @Override
790         public void onClick(View v) {
791             if (mRadioManager == null) {
792                 return;
793             }
794 
795             clearMetadataDisplay();
796 
797             if (!mHasDualTuners) {
798                 try {
799                     mRadioManager.seekBackward();
800                 } catch (RemoteException e) {
801                     Log.e(TAG, "backwardSeek(); remote exception: " + e.getMessage());
802                 }
803                 return;
804             }
805 
806             RadioStation prevStation = mAdapter.getPrevStation();
807 
808             if (prevStation != null) {
809                 if (Log.isLoggable(TAG, Log.DEBUG)) {
810                     Log.d(TAG, "Seek backwards to station: " + prevStation);
811                 }
812 
813                 // Tune to the previous station, and then update the UI to reflect that tune.
814                 try {
815                     mRadioManager.tune(prevStation);
816                 } catch (RemoteException e) {
817                     Log.e(TAG, "backwardSeek(); remote exception: " + e.getMessage());
818                 }
819 
820                 int position = mAdapter.getCurrentPosition();
821                 mRadioDisplayController.setCurrentStationInList(position);
822             }
823         }
824     };
825 
826     private final View.OnClickListener mForwardSeekClickListener = new View.OnClickListener() {
827         @Override
828         public void onClick(View v) {
829             if (mRadioManager == null) {
830                 return;
831             }
832 
833             clearMetadataDisplay();
834 
835             if (!mHasDualTuners) {
836                 try {
837                     mRadioManager.seekForward();
838                 } catch (RemoteException e) {
839                     Log.e(TAG, "forwardSeek(); remote exception: " + e.getMessage());
840                 }
841                 return;
842             }
843 
844             RadioStation nextStation = mAdapter.getNextStation();
845 
846             if (nextStation != null) {
847                 if (Log.isLoggable(TAG, Log.DEBUG)) {
848                     Log.d(TAG, "Seek forward to station: " + nextStation);
849                 }
850 
851                 // Tune to the next station, and then update the UI to reflect that tune.
852                 try {
853                     mRadioManager.tune(nextStation);
854                 } catch (RemoteException e) {
855                     Log.e(TAG, "forwardSeek(); remote exception: " + e.getMessage());
856                 }
857 
858                 int position = mAdapter.getCurrentPosition();
859                 mRadioDisplayController.setCurrentStationInList(position);
860             }
861         }
862     };
863 
864     /**
865      * Click listener for the play/pause button. Currently, all this does is mute/unmute the radio
866      * because the {@link RadioManager} does not support the ability to pause/start again.
867      */
868     private final View.OnClickListener mPlayPauseClickListener = new View.OnClickListener() {
869         @Override
870         public void onClick(View v) {
871             if (mRadioManager == null) {
872                 return;
873             }
874 
875             try {
876                 if (Log.isLoggable(TAG, Log.DEBUG)) {
877                     Log.d(TAG, "Play button clicked. Currently muted: " + mRadioManager.isMuted());
878                 }
879 
880                 if (mRadioManager.isMuted()) {
881                     mRadioManager.unMute();
882                 } else {
883                     mRadioManager.mute();
884                 }
885 
886                 boolean isMuted = mRadioManager.isMuted();
887 
888                 mUserHasMuted = isMuted;
889                 mRadioDisplayController.setPlayPauseButtonState(isMuted);
890             } catch (RemoteException e) {
891                 Log.e(TAG, "playPauseClickListener(); remote exception: " + e.getMessage());
892             }
893         }
894     };
895 
896     private final View.OnClickListener mPresetButtonClickListener = new View.OnClickListener() {
897         // TODO: Maybe add a check to send a store/remove preset event after a delay so that
898         // there aren't multiple writes if the user presses the button quickly.
899         @Override
900         public void onClick(View v) {
901             if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) {
902                 if (Log.isLoggable(TAG, Log.DEBUG)) {
903                     Log.d(TAG, "Attempting to store invalid radio station as a preset. Ignoring");
904                 }
905 
906                 return;
907             }
908 
909             RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */,
910                     mCurrentRadioBand, mCurrentRds);
911             boolean isPreset = mRadioStorage.isPreset(station);
912 
913             if (Log.isLoggable(TAG, Log.DEBUG)) {
914                 Log.d(TAG, "Toggling preset for " + station
915                         + "\n\tIs currently a preset: " + isPreset);
916             }
917 
918             if (isPreset) {
919                 mRadioStorage.removePreset(station);
920             } else {
921                 mRadioStorage.storePreset(station);
922             }
923 
924             // Update the UI immediately. If the preset failed for some reason, the RadioStorage
925             // will notify us and UI update will happen then.
926             mRadioDisplayController.setChannelIsPreset(!isPreset);
927         }
928     };
929 
930     private ServiceConnection mServiceConnection = new ServiceConnection() {
931         @Override
932         public void onServiceConnected(ComponentName className, IBinder binder) {
933             mRadioManager = ((IRadioManager) binder);
934 
935             try {
936                 if (mRadioManager == null || !mRadioManager.isInitialized()) {
937                     mRadioDisplayController.setEnabled(false);
938 
939                     if (mRadioErrorDisplay != null) {
940                         mRadioErrorDisplay.setVisibility(View.VISIBLE);
941                     }
942 
943                     return;
944                 }
945 
946                 mRadioDisplayController.setEnabled(true);
947 
948                 if (mRadioErrorDisplay != null) {
949                     mRadioErrorDisplay.setVisibility(View.GONE);
950                 }
951 
952                 mHasDualTuners = mRadioManager.hasDualTuners();
953 
954                 if (mHasDualTuners) {
955                     initializeDualTunerController();
956                 } else {
957                     mRadioDisplayController.setSingleChannelDisplay(mRadioBackground);
958                 }
959 
960                 mRadioManager.addRadioTunerCallback(mCallback);
961 
962                 int radioBand = mRadioStorage.getStoredRadioBand();
963 
964                 // Upon successful connection, open the radio.
965                 openRadioBand(radioBand);
966                 maybeTuneToStoredRadioChannel();
967 
968                 if (mStationChangeListener != null) {
969                     mStationChangeListener.onRadioStationChanged(
970                             mRadioManager.getCurrentRadioStation());
971                 }
972             } catch (RemoteException e) {
973                 Log.e(TAG, "onServiceConnected(); remote exception: " + e.getMessage());
974             }
975         }
976 
977         @Override
978         public void onServiceDisconnected(ComponentName className) {
979             mRadioManager = null;
980         }
981     };
982 
983     private final ValueAnimator.AnimatorUpdateListener mBackgroundColorUpdater =
984             new ValueAnimator.AnimatorUpdateListener() {
985                 @Override
986                 public void onAnimationUpdate(ValueAnimator animator) {
987                     int backgroundColor = (int) animator.getAnimatedValue();
988                     setBackgroundColor(backgroundColor);
989                 }
990             };
991 }
992