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
17package com.android.car.radio;
18
19import android.animation.ArgbEvaluator;
20import android.animation.ValueAnimator;
21import android.app.Activity;
22import android.app.LoaderManager;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.Loader;
27import android.content.ServiceConnection;
28import android.graphics.Color;
29import android.hardware.radio.RadioManager;
30import android.hardware.radio.RadioTuner;
31import android.media.AudioManager;
32import android.os.Bundle;
33import android.os.IBinder;
34import android.os.RemoteException;
35import android.support.annotation.ColorInt;
36import android.support.annotation.Nullable;
37import android.text.TextUtils;
38import android.util.Log;
39import android.view.View;
40import com.android.car.radio.service.IRadioCallback;
41import com.android.car.radio.service.IRadioManager;
42import com.android.car.radio.service.RadioRds;
43import com.android.car.radio.service.RadioStation;
44
45import java.util.ArrayList;
46import java.util.List;
47
48/**
49 * A controller that handles the display of metadata on the current radio station.
50 */
51public 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         */
131        void onRadioStationChanged(RadioStation station);
132    }
133
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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
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
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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
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
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
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