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 import com.android.car.radio.service.IRadioCallback;
     41 import com.android.car.radio.service.IRadioManager;
     42 import com.android.car.radio.service.RadioRds;
     43 import com.android.car.radio.service.RadioStation;
     44 
     45 import java.util.ArrayList;
     46 import java.util.List;
     47 
     48 /**
     49  * A controller that handles the display of metadata on the current radio station.
     50  */
     51 public class RadioController implements
     52         RadioStorage.PresetsChangeListener,
     53         RadioStorage.PreScannedChannelChangeListener,
     54         LoaderManager.LoaderCallbacks<List<RadioStation>> {
     55     private static final String TAG = "Em.RadioController";
     56     private static final int CHANNEL_LOADER_ID = 0;
     57 
     58     /**
     59      * The percentage by which to darken the color that should be set on the status bar.
     60      * This darkening gives the status bar the illusion that it is transparent.
     61      *
     62      * @see {@link RadioController#setShouldColorStatusBar(boolean)}
     63      */
     64     private static final float STATUS_BAR_DARKEN_PERCENTAGE = 0.4f;
     65 
     66     /**
     67      * The animation time for when the background of the radio shifts to a different color.
     68      */
     69     private static final int BACKGROUND_CHANGE_ANIM_TIME_MS = 450;
     70     private static final int INVALID_BACKGROUND_COLOR = 0;
     71 
     72     private final int CHANNEL_CHANGE_DURATION_MS = 200;
     73 
     74     private int mCurrentChannelNumber = RadioStorage.INVALID_RADIO_CHANNEL;
     75 
     76     private final Activity mActivity;
     77     private IRadioManager mRadioManager;
     78 
     79     private View mRadioBackground;
     80     private boolean mShouldColorStatusBar;
     81 
     82     /**
     83      * An additional layer on top of the background that should match the color of
     84      * {@link #mRadioBackground}. This view should only exist in the preset list. The reason this
     85      * layer cannot be transparent is because it needs to be elevated, and elevation does not
     86      * work if the background is undefined or transparent.
     87      */
     88     private View mRadioPresetBackground;
     89 
     90     private View mRadioErrorDisplay;
     91 
     92     private final RadioChannelColorMapper mColorMapper;
     93     @ColorInt private int mCurrentBackgroundColor = INVALID_BACKGROUND_COLOR;
     94 
     95     private PrescannedRadioStationAdapter mAdapter;
     96     private PreScannedChannelLoader mChannelLoader;
     97 
     98     private final RadioDisplayController mRadioDisplayController;
     99     private boolean mHasDualTuners;
    100 
    101     /**
    102      * Keeps track of if the user has manually muted the radio. This value is used to determine
    103      * whether or not to un-mute the radio after an {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT}
    104      * event has been received.
    105      */
    106     private boolean mUserHasMuted;
    107 
    108     private final RadioStorage mRadioStorage;
    109 
    110     /**
    111      * The current radio band. This value is one of the BAND_* values from {@link RadioManager}.
    112      * For example, {@link RadioManager#BAND_FM}.
    113      */
    114     private int mCurrentRadioBand = RadioStorage.INVALID_RADIO_BAND;
    115     private final String mAmBandString;
    116     private final String mFmBandString;
    117 
    118     private RadioRds mCurrentRds;
    119 
    120     private RadioStationChangeListener mStationChangeListener;
    121 
    122     /**
    123      * Interface for a class that will be notified when the current radio station has been changed.
    124      */
    125     public interface RadioStationChangeListener {
    126         /**
    127          * Called when the current radio station has changed in the radio.
    128          *
    129          * @param station The current radio station.
    130          */
    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