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