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.annotation.ColorInt;
     22 import android.annotation.NonNull;
     23 import android.annotation.Nullable;
     24 import android.app.Activity;
     25 import android.content.ComponentName;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.ServiceConnection;
     29 import android.graphics.Color;
     30 import android.hardware.radio.ProgramSelector;
     31 import android.hardware.radio.RadioManager;
     32 import android.hardware.radio.RadioManager.ProgramInfo;
     33 import android.hardware.radio.RadioMetadata;
     34 import android.hardware.radio.RadioTuner;
     35 import android.media.AudioManager;
     36 import android.os.IBinder;
     37 import android.os.RemoteException;
     38 import android.util.Log;
     39 import android.view.View;
     40 
     41 import com.android.car.broadcastradio.support.Program;
     42 import com.android.car.broadcastradio.support.platform.ProgramInfoExt;
     43 import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
     44 import com.android.car.radio.service.IRadioCallback;
     45 import com.android.car.radio.service.IRadioManager;
     46 import com.android.car.radio.storage.RadioStorage;
     47 import com.android.car.radio.utils.ProgramSelectorUtils;
     48 
     49 import java.util.ArrayList;
     50 import java.util.List;
     51 import java.util.Objects;
     52 
     53 /**
     54  * A controller that handles the display of metadata on the current radio station.
     55  */
     56 public class RadioController implements RadioStorage.PresetsChangeListener {
     57     private static final String TAG = "Em.RadioController";
     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 final ValueAnimator mAnimator = new ValueAnimator();
     76     private int mCurrentlyDisplayedChannel;  // for animation purposes
     77     private ProgramInfo mCurrentProgram;
     78 
     79     private final Activity mActivity;
     80     private IRadioManager mRadioManager;
     81 
     82     private View mRadioBackground;
     83     private boolean mShouldColorStatusBar;
     84     private boolean mShouldColorBackground;
     85 
     86     /**
     87      * An additional layer on top of the background that should match the color of
     88      * {@link #mRadioBackground}. This view should only exist in the preset list. The reason this
     89      * layer cannot be transparent is because it needs to be elevated, and elevation does not
     90      * work if the background is undefined or transparent.
     91      */
     92     private View mRadioPresetBackground;
     93 
     94     private View mRadioErrorDisplay;
     95 
     96     private final RadioChannelColorMapper mColorMapper;
     97     @ColorInt private int mCurrentBackgroundColor = INVALID_BACKGROUND_COLOR;
     98 
     99     private final RadioDisplayController mRadioDisplayController;
    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     private final String mAmBandString;
    111     private final String mFmBandString;
    112 
    113     private List<ProgramInfoChangeListener> mProgramInfoChangeListeners = new ArrayList<>();
    114     private List<RadioServiceConnectionListener> mRadioServiceConnectionListeners =
    115             new ArrayList<>();
    116 
    117     /**
    118      * Interface for a class that will be notified when the current radio station has been changed.
    119      */
    120     public interface ProgramInfoChangeListener {
    121         /**
    122          * Called when the current radio station has changed in the radio.
    123          *
    124          * @param info The current radio station.
    125          */
    126         void onProgramInfoChanged(@NonNull ProgramInfo info);
    127     }
    128 
    129     /**
    130      * Interface for a class that will be notified when RadioService is successfuly bound
    131      */
    132     public interface RadioServiceConnectionListener {
    133 
    134         /**
    135          * Called when the RadioService is successfully connected
    136          */
    137         void onRadioServiceConnected();
    138     }
    139 
    140     public RadioController(Activity activity) {
    141         mActivity = activity;
    142 
    143         mRadioDisplayController = new RadioDisplayController(mActivity);
    144         mColorMapper = RadioChannelColorMapper.getInstance(mActivity);
    145 
    146         mAmBandString = mActivity.getString(R.string.radio_am_text);
    147         mFmBandString = mActivity.getString(R.string.radio_fm_text);
    148 
    149         mRadioStorage = RadioStorage.getInstance(mActivity);
    150         mRadioStorage.addPresetsChangeListener(this);
    151         mShouldColorBackground = true;
    152     }
    153 
    154     /**
    155      * Initializes this {@link RadioController} to control the UI whose root is the given container.
    156      */
    157     public void initialize(View container) {
    158         mCurrentBackgroundColor = INVALID_BACKGROUND_COLOR;
    159 
    160         mRadioDisplayController.initialize(container);
    161 
    162         mRadioDisplayController.setBackwardSeekButtonListener(mBackwardSeekClickListener);
    163         mRadioDisplayController.setForwardSeekButtonListener(mForwardSeekClickListener);
    164         mRadioDisplayController.setPlayButtonListener(mPlayPauseClickListener);
    165         mRadioDisplayController.setAddPresetButtonListener(mPresetButtonClickListener);
    166 
    167         mRadioBackground = container;
    168         mRadioPresetBackground = container.findViewById(R.id.preset_current_card_container);
    169 
    170         mRadioErrorDisplay = container.findViewById(R.id.radio_error_display);
    171 
    172         updateRadioDisplay();
    173     }
    174 
    175     /**
    176      * Set whether or not this controller should also update the color of the status bar to match
    177      * the current background color of the radio. The color that will be set on the status bar
    178      * will be slightly darker, giving the illusion that the status bar is transparent.
    179      *
    180      * <p>This method is needed because of scene transitions. Scene transitions do not take into
    181      * account padding that is added programmatically. Since there is no way to get the height of
    182      * the status bar and set it in XML, it needs to be done in code. This breaks the scene
    183      * transition.
    184      *
    185      * <p>To make this work, the status bar is not actually translucent; it is colored to appear
    186      * that way via this method.
    187      */
    188     public void setShouldColorStatusBar(boolean shouldColorStatusBar) {
    189        mShouldColorStatusBar = shouldColorStatusBar;
    190     }
    191 
    192     /**
    193      * Set whether this controller should update the background color.
    194      * This behavior is enabled by defaullt
    195      */
    196     public void setShouldColorBackground(boolean shouldColorBackground) {
    197         mShouldColorBackground = shouldColorBackground;
    198     }
    199 
    200     /**
    201      * Adds a listener that will be notified whenever the radio station changes.
    202      */
    203     public void addProgramInfoChangeListener(ProgramInfoChangeListener listener) {
    204         mProgramInfoChangeListeners.add(listener);
    205     }
    206 
    207     /**
    208      * Removes a listener that will be notified whenever the radio station changes.
    209      */
    210     public void removeProgramInfoChangeListener(ProgramInfoChangeListener listener) {
    211         mProgramInfoChangeListeners.remove(listener);
    212     }
    213 
    214     /**
    215      * Sets the listeners that will be notified when the radio service is connected.
    216      */
    217     public void addRadioServiceConnectionListener(RadioServiceConnectionListener listener) {
    218         mRadioServiceConnectionListeners.add(listener);
    219     }
    220 
    221     /**
    222      * Removes a listener that will be notified when the radio service is connected.
    223      */
    224     public void removeRadioServiceConnectionListener(RadioServiceConnectionListener listener) {
    225         mRadioServiceConnectionListeners.remove(listener);
    226     }
    227 
    228     /**
    229      * Starts the controller to handle radio tuning. This method should be called to begin
    230      * radio playback.
    231      */
    232     public void start() {
    233         if (Log.isLoggable(TAG, Log.DEBUG)) {
    234             Log.d(TAG, "starting radio");
    235         }
    236 
    237         Intent bindIntent = new Intent(RadioService.ACTION_UI_SERVICE, null /* uri */,
    238                 mActivity, RadioService.class);
    239         if (!mActivity.bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) {
    240             Log.e(TAG, "Failed to connect to RadioService.");
    241         }
    242 
    243         updateRadioDisplay();
    244     }
    245 
    246     /**
    247      * Retrieves information about the current radio station from {@link #mRadioManager} and updates
    248      * the display of that information accordingly.
    249      */
    250     private void updateRadioDisplay() {
    251         if (mRadioManager == null) {
    252             return;
    253         }
    254 
    255         try {
    256             mRadioDisplayController.setSingleChannelDisplay(mRadioBackground);
    257 
    258             // Ensure the play button properly reflects the current mute state.
    259             mRadioDisplayController.setPlayPauseButtonState(mRadioManager.isMuted());
    260 
    261             // TODO(b/73950974): use callback only
    262             ProgramInfo current = mRadioManager.getCurrentProgramInfo();
    263             if (current != null) mCallback.onCurrentProgramInfoChanged(current);
    264         } catch (RemoteException e) {
    265             Log.e(TAG, "updateRadioDisplay(); remote exception: " + e.getMessage());
    266         }
    267     }
    268 
    269     /**
    270      * Tunes the radio to the given channel if it is valid and a {@link RadioTuner} has been opened.
    271      */
    272     public void tune(ProgramSelector sel) {
    273         if (mRadioManager == null) return;
    274 
    275         try {
    276             mRadioManager.tune(sel);
    277         } catch (RemoteException ex) {
    278             Log.e(TAG, "Failed to tune", ex);
    279         }
    280     }
    281 
    282     /**
    283      * Returns the band this radio is currently tuned to.
    284      *
    285      * TODO(b/73950974): don't be AM/FM exclusive
    286      */
    287     public int getCurrentRadioBand() {
    288         return ProgramSelectorUtils.getRadioBand(mCurrentProgram.getSelector());
    289     }
    290 
    291     /**
    292      * Returns the radio station that is currently playing on the radio. If this controller is
    293      * not connected to the {@link RadioService} or a radio station cannot be retrieved, then
    294      * {@code null} is returned.
    295      *
    296      * TODO(b/73950974): use callback only
    297      */
    298     @Nullable
    299     public ProgramInfo getCurrentProgramInfo() {
    300         return mCurrentProgram;
    301     }
    302 
    303     /**
    304      * Switch radio band. Currently, this only supports FM and AM bands.
    305      *
    306      * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM}.
    307      */
    308     public void switchBand(int radioBand) {
    309         try {
    310             mRadioManager.switchBand(radioBand);
    311         } catch (RemoteException e) {
    312             Log.e(TAG, "Couldn't switch band", e);
    313         }
    314     }
    315 
    316     /**
    317      * Delegates to the {@link RadioDisplayController} to highlight the radio band.
    318      */
    319     private void updateAmFmDisplayState(int band) {
    320         switch (band) {
    321             case RadioManager.BAND_FM:
    322                 mRadioDisplayController.setChannelBand(mFmBandString);
    323                 break;
    324 
    325             case RadioManager.BAND_AM:
    326                 mRadioDisplayController.setChannelBand(mAmBandString);
    327                 break;
    328 
    329             // TODO: Support BAND_FM_HD and BAND_AM_HD.
    330 
    331             default:
    332                 mRadioDisplayController.setChannelBand(null);
    333         }
    334     }
    335 
    336     // TODO(b/73950974): move channel animation to RadioDisplayController
    337     private void updateRadioChannelDisplay(@NonNull ProgramSelector sel) {
    338         int priType = sel.getPrimaryId().getType();
    339 
    340         mAnimator.cancel();
    341 
    342         if (!ProgramSelectorExt.isAmFmProgram(sel)
    343                 || !ProgramSelectorExt.hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) {
    344             // channel animation is implemented for AM/FM only
    345             mCurrentlyDisplayedChannel = 0;
    346             mRadioDisplayController.setChannelNumber("");
    347 
    348             updateAmFmDisplayState(RadioStorage.INVALID_RADIO_BAND);
    349             return;
    350         }
    351 
    352         int freq = (int)sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
    353 
    354         boolean wasAm = ProgramSelectorExt.isAmFrequency(mCurrentlyDisplayedChannel);
    355         boolean wasFm = ProgramSelectorExt.isFmFrequency(mCurrentlyDisplayedChannel);
    356         boolean isAm = ProgramSelectorExt.isAmFrequency(freq);
    357         int band = isAm ? RadioManager.BAND_AM : RadioManager.BAND_FM;
    358 
    359         updateAmFmDisplayState(band);
    360 
    361         if (isAm && wasAm || !isAm && wasFm) {
    362             mAnimator.setIntValues((int)mCurrentlyDisplayedChannel, (int)freq);
    363             mAnimator.setDuration(CHANNEL_CHANGE_DURATION_MS);
    364             mAnimator.addUpdateListener(animation -> mRadioDisplayController.setChannelNumber(
    365                     ProgramSelectorExt.formatAmFmFrequency((int)animation.getAnimatedValue(),
    366                             ProgramSelectorExt.NAME_NO_MODULATION)));
    367             mAnimator.start();
    368         } else {
    369             // it's a different band - don't animate
    370             mRadioDisplayController.setChannelNumber(
    371                     ProgramSelectorExt.getDisplayName(sel, ProgramSelectorExt.NAME_NO_MODULATION));
    372         }
    373         mCurrentlyDisplayedChannel = freq;
    374 
    375         maybeUpdateBackgroundColor(freq);
    376     }
    377 
    378     /**
    379      * Checks if the color of the radio background should be changed, and if so, animates that
    380      * color change.
    381      */
    382     private void maybeUpdateBackgroundColor(int channel) {
    383         if (mRadioBackground == null || !mShouldColorBackground) {
    384             return;
    385         }
    386 
    387         int newColor = mColorMapper.getColorForChannel(channel);
    388 
    389         // No animation required if the colors are the same.
    390         if (newColor == mCurrentBackgroundColor) {
    391             return;
    392         }
    393 
    394         // If the current background color is invalid, then just set as the new color without any
    395         // animation.
    396         if (mCurrentBackgroundColor == INVALID_BACKGROUND_COLOR) {
    397             mCurrentBackgroundColor = newColor;
    398             setBackgroundColor(newColor);
    399         }
    400 
    401         // Otherwise, animate the background color change.
    402         ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
    403                 mCurrentBackgroundColor, newColor);
    404         colorAnimation.setDuration(BACKGROUND_CHANGE_ANIM_TIME_MS);
    405         colorAnimation.addUpdateListener(mBackgroundColorUpdater);
    406         colorAnimation.start();
    407 
    408         mCurrentBackgroundColor = newColor;
    409     }
    410 
    411     private void setBackgroundColor(int backgroundColor) {
    412         mRadioBackground.setBackgroundColor(backgroundColor);
    413 
    414         if (mRadioPresetBackground != null) {
    415             mRadioPresetBackground.setBackgroundColor(backgroundColor);
    416         }
    417 
    418         if (mShouldColorStatusBar) {
    419             int red = darkenColor(Color.red(backgroundColor));
    420             int green = darkenColor(Color.green(backgroundColor));
    421             int blue = darkenColor(Color.blue(backgroundColor));
    422             int alpha = Color.alpha(backgroundColor);
    423 
    424             mActivity.getWindow().setStatusBarColor(
    425                     Color.argb(alpha, red, green, blue));
    426         }
    427     }
    428 
    429     /**
    430      * Darkens the given color by {@link #STATUS_BAR_DARKEN_PERCENTAGE}.
    431      */
    432     private int darkenColor(int color) {
    433         return (int) Math.max(color - (color * STATUS_BAR_DARKEN_PERCENTAGE), 0);
    434     }
    435 
    436     /**
    437      * Clears all metadata including song title, artist and station information.
    438      */
    439     private void clearMetadataDisplay() {
    440         mRadioDisplayController.setCurrentStation(null);
    441         mRadioDisplayController.setCurrentSongTitleAndArtist(null, null);
    442     }
    443 
    444     /**
    445      * Closes any active {@link RadioTuner}s and releases audio focus.
    446      */
    447     private void close() {
    448         if (Log.isLoggable(TAG, Log.DEBUG)) {
    449             Log.d(TAG, "close()");
    450         }
    451 
    452         // Lost focus, so display that the radio is not playing anymore.
    453         mRadioDisplayController.setPlayPauseButtonState(true);
    454     }
    455 
    456     /**
    457      * Closes all active connections in the {@link RadioController}.
    458      */
    459     public void shutdown() {
    460         if (Log.isLoggable(TAG, Log.DEBUG)) {
    461             Log.d(TAG, "shutdown()");
    462         }
    463 
    464         mActivity.unbindService(mServiceConnection);
    465         mRadioStorage.removePresetsChangeListener(this);
    466 
    467         if (mRadioManager != null) {
    468             try {
    469                 mRadioManager.removeRadioTunerCallback(mCallback);
    470             } catch (RemoteException e) {
    471                 Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage());
    472             }
    473         }
    474 
    475         close();
    476     }
    477 
    478     @Override
    479     public void onPresetsRefreshed() {
    480         // Check if the current channel's preset status has changed.
    481         ProgramInfo info = mCurrentProgram;
    482         boolean isPreset = (info != null) && mRadioStorage.isPreset(info.getSelector());
    483         mRadioDisplayController.setChannelIsPreset(isPreset);
    484     }
    485 
    486     /**
    487      * Gets a list of programs from the radio tuner's background scan
    488      */
    489     public List<ProgramInfo> getProgramList() {
    490         if (mRadioManager != null) {
    491             try {
    492                 return mRadioManager.getProgramList();
    493             } catch (RemoteException e) {
    494                 Log.e(TAG, "getProgramList(); remote exception: " + e.getMessage());
    495             }
    496         }
    497         return null;
    498     }
    499 
    500     private final IRadioCallback.Stub mCallback = new IRadioCallback.Stub() {
    501         @Override
    502         public void onCurrentProgramInfoChanged(ProgramInfo info) {
    503             mCurrentProgram = Objects.requireNonNull(info);
    504             ProgramSelector sel = info.getSelector();
    505 
    506             updateRadioChannelDisplay(sel);
    507 
    508             mRadioDisplayController.setCurrentStation(
    509                     ProgramInfoExt.getProgramName(info, ProgramInfoExt.NAME_NO_CHANNEL_FALLBACK));
    510             RadioMetadata meta = ProgramInfoExt.getMetadata(mCurrentProgram);
    511             mRadioDisplayController.setCurrentSongTitleAndArtist(
    512                     meta.getString(RadioMetadata.METADATA_KEY_TITLE),
    513                     meta.getString(RadioMetadata.METADATA_KEY_ARTIST));
    514 
    515             mRadioDisplayController.setChannelIsPreset(mRadioStorage.isPreset(sel));
    516 
    517             // Notify that the current radio station has changed.
    518             if (mProgramInfoChangeListeners != null) {
    519                 for (ProgramInfoChangeListener listener : mProgramInfoChangeListeners) {
    520                     listener.onProgramInfoChanged(info);
    521                 }
    522             }
    523         }
    524 
    525         @Override
    526         public void onRadioMuteChanged(boolean isMuted) {
    527             mRadioDisplayController.setPlayPauseButtonState(isMuted);
    528         }
    529 
    530         @Override
    531         public void onError(int status) {
    532             Log.e(TAG, "Radio callback error with status: " + status);
    533             close();
    534         }
    535     };
    536 
    537     private final View.OnClickListener mBackwardSeekClickListener = new View.OnClickListener() {
    538         @Override
    539         public void onClick(View v) {
    540             if (mRadioManager == null) return;
    541 
    542             // TODO(b/73950974): show some kind of animation
    543             clearMetadataDisplay();
    544 
    545             try {
    546                 // TODO(b/73950974): watch for timeout and if it happens, display metadata back
    547                 mRadioManager.seekBackward();
    548             } catch (RemoteException e) {
    549                 Log.e(TAG, "backwardSeek(); remote exception: " + e.getMessage());
    550             }
    551         }
    552     };
    553 
    554     private final View.OnClickListener mForwardSeekClickListener = new View.OnClickListener() {
    555         @Override
    556         public void onClick(View v) {
    557             if (mRadioManager == null) return;
    558 
    559             clearMetadataDisplay();
    560 
    561             try {
    562                 mRadioManager.seekForward();
    563             } catch (RemoteException e) {
    564                 Log.e(TAG, "Couldn't seek forward", e);
    565             }
    566         }
    567     };
    568 
    569     /**
    570      * Click listener for the play/pause button. Currently, all this does is mute/unmute the radio
    571      * because the {@link RadioManager} does not support the ability to pause/start again.
    572      */
    573     private final View.OnClickListener mPlayPauseClickListener = new View.OnClickListener() {
    574         @Override
    575         public void onClick(View v) {
    576             if (mRadioManager == null) {
    577                 return;
    578             }
    579 
    580             try {
    581                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    582                     Log.d(TAG, "Play button clicked. Currently muted: " + mRadioManager.isMuted());
    583                 }
    584 
    585                 if (mRadioManager.isMuted()) {
    586                     mRadioManager.unMute();
    587                 } else {
    588                     mRadioManager.mute();
    589                 }
    590 
    591                 boolean isMuted = mRadioManager.isMuted();
    592 
    593                 mUserHasMuted = isMuted;
    594                 mRadioDisplayController.setPlayPauseButtonState(isMuted);
    595             } catch (RemoteException e) {
    596                 Log.e(TAG, "playPauseClickListener(); remote exception: " + e.getMessage());
    597             }
    598         }
    599     };
    600 
    601     private final View.OnClickListener mPresetButtonClickListener = new View.OnClickListener() {
    602         // TODO: Maybe add a check to send a store/remove preset event after a delay so that
    603         // there aren't multiple writes if the user presses the button quickly.
    604         @Override
    605         public void onClick(View v) {
    606             ProgramInfo info = mCurrentProgram;
    607             if (info == null) return;
    608 
    609             ProgramSelector sel = mCurrentProgram.getSelector();
    610             boolean isPreset = mRadioStorage.isPreset(sel);
    611 
    612             if (isPreset) {
    613                 mRadioStorage.removePreset(sel);
    614             } else {
    615                 mRadioStorage.storePreset(Program.fromProgramInfo(info));
    616             }
    617 
    618             // Update the UI immediately. If the preset failed for some reason, the RadioStorage
    619             // will notify us and UI update will happen then.
    620             mRadioDisplayController.setChannelIsPreset(!isPreset);
    621         }
    622     };
    623 
    624     private ServiceConnection mServiceConnection = new ServiceConnection() {
    625         @Override
    626         public void onServiceConnected(ComponentName className, IBinder binder) {
    627             mRadioManager = ((IRadioManager) binder);
    628 
    629             try {
    630                 if (mRadioManager == null || !mRadioManager.isInitialized()) {
    631                     mRadioDisplayController.setEnabled(false);
    632 
    633                     if (mRadioErrorDisplay != null) {
    634                         mRadioErrorDisplay.setVisibility(View.VISIBLE);
    635                     }
    636 
    637                     return;
    638                 }
    639 
    640                 mRadioDisplayController.setEnabled(true);
    641 
    642                 if (mRadioErrorDisplay != null) {
    643                     mRadioErrorDisplay.setVisibility(View.GONE);
    644                 }
    645 
    646                 mRadioDisplayController.setSingleChannelDisplay(mRadioBackground);
    647 
    648                 mRadioManager.addRadioTunerCallback(mCallback);
    649 
    650                 // Notify listeners
    651                 for (RadioServiceConnectionListener listener : mRadioServiceConnectionListeners) {
    652                     listener.onRadioServiceConnected();
    653                 }
    654             } catch (RemoteException e) {
    655                 Log.e(TAG, "onServiceConnected(); remote exception: " + e.getMessage());
    656             }
    657         }
    658 
    659         @Override
    660         public void onServiceDisconnected(ComponentName className) {
    661             mRadioManager = null;
    662         }
    663     };
    664 
    665     private final ValueAnimator.AnimatorUpdateListener mBackgroundColorUpdater =
    666             animator -> {
    667                 int backgroundColor = (int) animator.getAnimatedValue();
    668                 setBackgroundColor(backgroundColor);
    669             };
    670 }
    671