Home | History | Annotate | Download | only in radio
      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 
     41 import com.android.car.radio.service.IRadioCallback;
     42 import com.android.car.radio.service.IRadioManager;
     43 import com.android.car.radio.service.RadioRds;
     44 import com.android.car.radio.service.RadioStation;
     45 
     46 import java.util.ArrayList;
     47 import java.util.List;
     48 
     49 /**
     50  * A controller that handles the display of metadata on the current radio station.
     51  */
     52 public 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