• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
onProgramInfoChanged(@onNull ProgramInfo info)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          */
onRadioServiceConnected()137         void onRadioServiceConnected();
138     }
139 
RadioController(Activity activity)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      */
initialize(View container)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      */
setShouldColorStatusBar(boolean shouldColorStatusBar)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      */
setShouldColorBackground(boolean shouldColorBackground)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      */
addProgramInfoChangeListener(ProgramInfoChangeListener listener)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      */
removeProgramInfoChangeListener(ProgramInfoChangeListener listener)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      */
addRadioServiceConnectionListener(RadioServiceConnectionListener listener)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      */
removeRadioServiceConnectionListener(RadioServiceConnectionListener listener)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      */
start()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      */
updateRadioDisplay()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      */
tune(ProgramSelector sel)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      */
getCurrentRadioBand()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
getCurrentProgramInfo()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      */
switchBand(int radioBand)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      */
updateAmFmDisplayState(int band)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
updateRadioChannelDisplay(@onNull ProgramSelector sel)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      */
maybeUpdateBackgroundColor(int channel)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 
setBackgroundColor(int backgroundColor)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      */
darkenColor(int color)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      */
clearMetadataDisplay()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      */
close()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      */
shutdown()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
onPresetsRefreshed()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      */
getProgramList()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