• 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.app.Service;
20 import android.car.hardware.radio.CarRadioManager;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.hardware.radio.RadioManager;
25 import android.hardware.radio.RadioMetadata;
26 import android.hardware.radio.RadioTuner;
27 import android.media.AudioAttributes;
28 import android.media.AudioManager;
29 import android.os.Handler;
30 import android.os.IBinder;
31 import android.os.RemoteException;
32 import android.os.SystemProperties;
33 import android.support.annotation.Nullable;
34 import android.support.car.Car;
35 import android.support.car.CarNotConnectedException;
36 import android.support.car.CarConnectionCallback;
37 import android.support.car.media.CarAudioManager;
38 import android.text.TextUtils;
39 import android.util.Log;
40 import com.android.car.radio.demo.RadioDemo;
41 import com.android.car.radio.service.IRadioCallback;
42 import com.android.car.radio.service.IRadioManager;
43 import com.android.car.radio.service.RadioRds;
44 import com.android.car.radio.service.RadioStation;
45 
46 import java.util.ArrayList;
47 import java.util.List;
48 
49 /**
50  * A persistent {@link Service} that is responsible for opening and closing a {@link RadioTuner}.
51  * All radio operations should be delegated to this class. To be notified of any changes in radio
52  * metadata, register as a {@link android.hardware.radio.RadioTuner.Callback} on this Service.
53  *
54  * <p>Utilize the {@link RadioBinder} to perform radio operations.
55  */
56 public class RadioService extends Service implements AudioManager.OnAudioFocusChangeListener {
57     private static String TAG = "Em.RadioService";
58 
59     /**
60      * The amount of time to wait before re-trying to open the {@link #mRadioTuner}.
61      */
62     private static final int RADIO_TUNER_REOPEN_DELAY_MS = 5000;
63 
64     private int mReOpenRadioTunerCount = 0;
65     private final Handler mHandler = new Handler();
66 
67     private Car mCarApi;
68     private RadioTuner mRadioTuner;
69 
70     private boolean mRadioSuccessfullyInitialized;
71     private int mCurrentRadioBand = RadioManager.BAND_FM;
72     private int mCurrentRadioChannel = RadioStorage.INVALID_RADIO_CHANNEL;
73 
74     private String mCurrentChannelInfo;
75     private String mCurrentArtist;
76     private String mCurrentSongTitle;
77 
78     private RadioManager mRadioManager;
79     private RadioBackgroundScanner mBackgroundScanner;
80     private RadioManager.FmBandDescriptor mFmDescriptor;
81     private RadioManager.AmBandDescriptor mAmDescriptor;
82 
83     private RadioManager.FmBandConfig mFmConfig;
84     private RadioManager.AmBandConfig mAmConfig;
85 
86     private final List<RadioManager.ModuleProperties> mModules = new ArrayList<>();
87 
88     private CarAudioManager mCarAudioManager;
89     private AudioAttributes mRadioAudioAttributes;
90 
91     /**
92      * Whether or not this {@link RadioService} currently has audio focus, meaning it is the
93      * primary driver of media. Usually, interaction with the radio will be prefaced with an
94      * explicit request for audio focus. However, this is not ideal when muting the radio, so this
95      * state needs to be tracked.
96      */
97     private boolean mHasAudioFocus;
98 
99     /**
100      * An internal {@link android.hardware.radio.RadioTuner.Callback} that will listen for
101      * changes in radio metadata and pass these method calls through to
102      * {@link #mRadioTunerCallbacks}.
103      */
104     private RadioTuner.Callback mInternalRadioTunerCallback = new InternalRadioCallback();
105     private List<IRadioCallback> mRadioTunerCallbacks = new ArrayList<>();
106 
107     @Override
onBind(Intent intent)108     public IBinder onBind(Intent intent) {
109         if (Log.isLoggable(TAG, Log.DEBUG)) {
110             Log.d(TAG, "onBind(); Intent: " + intent);
111         }
112         return mBinder;
113     }
114 
115     @Override
onCreate()116     public void onCreate() {
117         super.onCreate();
118 
119         if (Log.isLoggable(TAG, Log.DEBUG)) {
120             Log.d(TAG, "onCreate()");
121         }
122 
123         // Connection to car services does not work for non-automotive yet, so this call needs to
124         // be guarded.
125         if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
126             mCarApi = Car.createCar(this /* context */, mCarConnectionCallback);
127             mCarApi.connect();
128         }
129 
130         if (SystemProperties.getBoolean(RadioDemo.DEMO_MODE_PROPERTY, false)) {
131             initializeDemo();
132         } else {
133             initialze();
134         }
135     }
136 
137     /**
138      * Initializes this service to use a demo {@link IRadioManager}.
139      *
140      * @see {@link RadioDemo}
141      */
initializeDemo()142     private void initializeDemo() {
143         if (Log.isLoggable(TAG, Log.DEBUG)) {
144             Log.d(TAG, "initializeDemo()");
145         }
146 
147         mBinder = RadioDemo.getInstance(this /* context */).createDemoManager();
148     }
149 
150     /**
151      * Connects to the {@link RadioManager}.
152      */
initialze()153     private void initialze() {
154         mRadioManager = (RadioManager) getSystemService(Context.RADIO_SERVICE);
155 
156         if (Log.isLoggable(TAG, Log.DEBUG)) {
157             Log.d(TAG, "initialze(); mRadioManager: " + mRadioManager);
158         }
159 
160         if (mRadioManager == null) {
161             Log.w(TAG, "RadioManager could not be loaded.");
162             return;
163         }
164 
165         int status = mRadioManager.listModules(mModules);
166         if (status != RadioManager.STATUS_OK) {
167             Log.w(TAG, "Load modules failed with status: " + status);
168             return;
169         }
170 
171         if (Log.isLoggable(TAG, Log.DEBUG)) {
172             Log.d(TAG, "initialze(); listModules complete: " + mModules);
173         }
174 
175         if (mModules.size() == 0) {
176             Log.w(TAG, "No radio modules on device.");
177             return;
178         }
179 
180         boolean isDebugLoggable = Log.isLoggable(TAG, Log.DEBUG);
181 
182         // Load the possible radio bands. For now, just accept FM and AM bands.
183         for (RadioManager.BandDescriptor band : mModules.get(0).getBands()) {
184             if (isDebugLoggable) {
185                 Log.d(TAG, "loading band: " + band.toString());
186             }
187 
188             if (mFmDescriptor == null && band.getType() == RadioManager.BAND_FM) {
189                 mFmDescriptor = (RadioManager.FmBandDescriptor) band;
190             }
191 
192             if (mAmDescriptor == null && band.getType() == RadioManager.BAND_AM) {
193                 mAmDescriptor = (RadioManager.AmBandDescriptor) band;
194             }
195         }
196 
197         if (mFmDescriptor == null && mAmDescriptor == null) {
198             Log.w(TAG, "No AM and FM radio bands could be loaded.");
199             return;
200         }
201 
202         // TODO: Make stereo configurable depending on device.
203         mFmConfig = new RadioManager.FmBandConfig.Builder(mFmDescriptor)
204                 .setStereo(true)
205                 .build();
206         mAmConfig = new RadioManager.AmBandConfig.Builder(mAmDescriptor)
207                 .setStereo(true)
208                 .build();
209 
210         // If there is a second tuner on the device, then set it up as the background scanner.
211         if (mModules.size() >= 2) {
212             if (isDebugLoggable) {
213                 Log.d(TAG, "Second tuner detected on device; setting up background scanner");
214             }
215 
216             mBackgroundScanner = new RadioBackgroundScanner(this /* context */, mRadioManager,
217                     mAmConfig, mFmConfig, mModules.get(1));
218         }
219 
220         mRadioSuccessfullyInitialized = true;
221     }
222 
223     @Override
onDestroy()224     public void onDestroy() {
225         if (Log.isLoggable(TAG, Log.DEBUG)) {
226             Log.d(TAG, "onDestroy()");
227         }
228 
229         close();
230 
231         if (mCarApi != null) {
232             mCarApi.disconnect();
233         }
234 
235         super.onDestroy();
236     }
237 
238     /**
239      * Opens the current radio band. Currently, this only supports FM and AM bands.
240      *
241      * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
242      *                  {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
243      * @return {@link RadioManager#STATUS_OK} if successful; otherwise,
244      * {@link RadioManager#STATUS_ERROR}.
245      */
openRadioBandInternal(int radioBand)246     private int openRadioBandInternal(int radioBand) {
247         if (requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
248             Log.e(TAG, "openRadioBandInternal() audio focus request fail");
249             return RadioManager.STATUS_ERROR;
250         }
251 
252         mCurrentRadioBand = radioBand;
253         RadioManager.BandConfig config = getRadioConfig(radioBand);
254 
255         if (config == null) {
256             Log.w(TAG, "Cannot create config for radio band: " + radioBand);
257             return RadioManager.STATUS_ERROR;
258         }
259 
260         if (mRadioTuner != null) {
261             mRadioTuner.setConfiguration(config);
262         } else {
263             mRadioTuner = mRadioManager.openTuner(mModules.get(0).getId(), config, true,
264                     mInternalRadioTunerCallback, null /* handler */);
265         }
266 
267         if (Log.isLoggable(TAG, Log.DEBUG)) {
268             Log.d(TAG, "openRadioBandInternal() STATUS_OK");
269         }
270 
271         if (mBackgroundScanner != null) {
272             mBackgroundScanner.onRadioBandChanged(radioBand);
273         }
274 
275         // Reset the counter for exponential backoff each time the radio tuner has been successfully
276         // opened.
277         mReOpenRadioTunerCount = 0;
278 
279         return RadioManager.STATUS_OK;
280     }
281 
282     /**
283      * Returns a {@link RadioRds} object that holds all the current radio metadata. If all the
284      * metadata is empty, then {@code null} is returned.
285      */
286     @Nullable
createCurrentRadioRds()287     private RadioRds createCurrentRadioRds() {
288         if (TextUtils.isEmpty(mCurrentChannelInfo) && TextUtils.isEmpty(mCurrentArtist)
289                 && TextUtils.isEmpty(mCurrentSongTitle)) {
290             return null;
291         }
292 
293         return new RadioRds(mCurrentChannelInfo, mCurrentArtist, mCurrentSongTitle);
294     }
295 
296     /**
297      * Creates a {@link RadioStation} that encapsulates all the information about the current
298      * radio station.
299      */
createCurrentRadioStation()300     private RadioStation createCurrentRadioStation() {
301         // mCurrentRadioChannel can possibly be invalid if this class never receives a callback
302         // for onProgramInfoChanged(). As a result, manually retrieve the information for the
303         // current station from RadioTuner if this is the case.
304         if (mCurrentRadioChannel == RadioStorage.INVALID_RADIO_CHANNEL && mRadioTuner != null) {
305             if (Log.isLoggable(TAG, Log.DEBUG)) {
306                 Log.d(TAG, "createCurrentRadioStation(); invalid current radio channel. "
307                         + "Calling getProgramInformation for valid station");
308             }
309 
310             // getProgramInformation() expects an array of size 1.
311             RadioManager.ProgramInfo[] info = new RadioManager.ProgramInfo[1];
312             int status = mRadioTuner.getProgramInformation(info);
313 
314             if (Log.isLoggable(TAG, Log.DEBUG)) {
315                 Log.d(TAG, "getProgramInformation() status: " + status + "; info: " + info[0]);
316             }
317 
318             if (status == RadioManager.STATUS_OK && info[0] != null) {
319                 mCurrentRadioChannel = info[0].getChannel();
320 
321                 if (Log.isLoggable(TAG, Log.DEBUG)) {
322                     Log.d(TAG, "program info channel: " + mCurrentRadioChannel);
323                 }
324             }
325         }
326 
327         return new RadioStation(mCurrentRadioChannel, 0 /* subChannelNumber */,
328                 mCurrentRadioBand, createCurrentRadioRds());
329     }
330 
331     /**
332      * Returns the proper {@link android.hardware.radio.RadioManager.BandConfig} for the given
333      * radio band. {@code null} is returned if the band is not suppored.
334      */
335     @Nullable
getRadioConfig(int selectedRadioBand)336     private RadioManager.BandConfig getRadioConfig(int selectedRadioBand) {
337         switch (selectedRadioBand) {
338             case RadioManager.BAND_AM:
339                 return mAmConfig;
340             case RadioManager.BAND_FM:
341                 return mFmConfig;
342 
343             // TODO: Support BAND_FM_HD and BAND_AM_HD.
344 
345             default:
346                 return null;
347         }
348     }
349 
requestAudioFocus()350     private int requestAudioFocus() {
351         int status = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
352         try {
353             status = mCarAudioManager.requestAudioFocus(this, mRadioAudioAttributes,
354                     AudioManager.AUDIOFOCUS_GAIN, 0);
355         } catch (CarNotConnectedException e) {
356             Log.e(TAG, "requestAudioFocus() failed", e);
357         }
358 
359         if (Log.isLoggable(TAG, Log.DEBUG)) {
360             Log.d(TAG, "requestAudioFocus status: " + status);
361         }
362 
363         if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
364             mHasAudioFocus = true;
365 
366             // Receiving audio focus means that the radio is un-muted.
367             for (IRadioCallback callback : mRadioTunerCallbacks) {
368                 try {
369                     callback.onRadioMuteChanged(false);
370                 } catch (RemoteException e) {
371                     Log.e(TAG, "requestAudioFocus(); onRadioMuteChanged() notify failed: "
372                             + e.getMessage());
373                 }
374             }
375         }
376 
377         return status;
378     }
379 
abandonAudioFocus()380     private void abandonAudioFocus() {
381         if (Log.isLoggable(TAG, Log.DEBUG)) {
382             Log.d(TAG, "abandonAudioFocus()");
383         }
384 
385         if (mCarAudioManager == null) {
386             return;
387         }
388 
389         mCarAudioManager.abandonAudioFocus(this, mRadioAudioAttributes);
390         mHasAudioFocus = false;
391 
392         for (IRadioCallback callback : mRadioTunerCallbacks) {
393             try {
394                 callback.onRadioMuteChanged(true);
395             } catch (RemoteException e) {
396                 Log.e(TAG, "abandonAudioFocus(); onRadioMutechanged() notify failed: "
397                         + e.getMessage());
398             }
399         }
400     }
401 
402     /**
403      * Closes any active {@link RadioTuner}s and releases audio focus.
404      */
close()405     private void close() {
406         if (Log.isLoggable(TAG, Log.DEBUG)) {
407             Log.d(TAG, "close()");
408         }
409 
410         abandonAudioFocus();
411 
412         if (mRadioTuner != null) {
413             mRadioTuner.close();
414             mRadioTuner = null;
415         }
416     }
417 
418     @Override
onAudioFocusChange(int focusChange)419     public void onAudioFocusChange(int focusChange) {
420         if (Log.isLoggable(TAG, Log.DEBUG)) {
421             Log.d(TAG, "focus change: " + focusChange);
422         }
423 
424         switch (focusChange) {
425             case AudioManager.AUDIOFOCUS_GAIN:
426                 mHasAudioFocus = true;
427                 openRadioBandInternal(mCurrentRadioBand);
428                 break;
429 
430             // For a transient loss, just allow the focus to be released. The radio will stop
431             // itself automatically. There is no need for an explicit abandon audio focus call
432             // because this removes the AudioFocusChangeListener.
433             case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
434             case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
435                 mHasAudioFocus = false;
436                 break;
437 
438             case AudioManager.AUDIOFOCUS_LOSS:
439                 close();
440                 break;
441 
442             default:
443                 // Do nothing for all other cases.
444         }
445     }
446 
447     /**
448      * {@link CarConnectionCallback} that retrieves the {@link CarRadioManager}.
449      */
450     private final CarConnectionCallback mCarConnectionCallback =
451             new CarConnectionCallback() {
452                 @Override
453                 public void onConnected(Car car) {
454                     if (Log.isLoggable(TAG, Log.DEBUG)) {
455                         Log.d(TAG, "Car service connected.");
456                     }
457                     try {
458                         // The CarAudioManager only needs to be retrieved once.
459                         if (mCarAudioManager == null) {
460                             mCarAudioManager = (CarAudioManager) mCarApi.getCarManager(
461                                     android.car.Car.AUDIO_SERVICE);
462 
463                             mRadioAudioAttributes = mCarAudioManager.getAudioAttributesForCarUsage(
464                                     CarAudioManager.CAR_AUDIO_USAGE_RADIO);
465                         }
466                     } catch (CarNotConnectedException e) {
467                         //TODO finish
468                         Log.e(TAG, "Car not connected");
469                     }
470                 }
471 
472                 @Override
473                 public void onDisconnected(Car car) {
474                     if (Log.isLoggable(TAG, Log.DEBUG)) {
475                         Log.d(TAG, "Car service disconnected.");
476                     }
477                 }
478             };
479 
480     private IRadioManager.Stub mBinder = new IRadioManager.Stub() {
481         /**
482          * Tunes the radio to the given frequency. To be notified of a successful tune, register
483          * as a {@link android.hardware.radio.RadioTuner.Callback}.
484          */
485         @Override
486         public void tune(RadioStation radioStation) {
487             if (mRadioManager == null || radioStation == null
488                     || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
489                 return;
490             }
491 
492             if (mRadioTuner == null || radioStation.getRadioBand() != mCurrentRadioBand) {
493                 int radioStatus = openRadioBandInternal(radioStation.getRadioBand());
494                 if (radioStatus == RadioManager.STATUS_ERROR) {
495                     return;
496                 }
497             }
498 
499             int status = mRadioTuner.tune(radioStation.getChannelNumber(), 0 /* subChannel */);
500 
501             if (Log.isLoggable(TAG, Log.DEBUG)) {
502                 Log.d(TAG, "Tuning to station: " + radioStation + "\n\tstatus: " + status);
503             }
504         }
505 
506         /**
507          * Seeks the radio forward. To be notified of a successful tune, register as a
508          * {@link android.hardware.radio.RadioTuner.Callback}.
509          */
510         @Override
511         public void seekForward() {
512             if (mRadioManager == null
513                     || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
514                 return;
515             }
516 
517             if (mRadioTuner == null) {
518                 int radioStatus = openRadioBandInternal(mCurrentRadioBand);
519                 if (radioStatus == RadioManager.STATUS_ERROR) {
520                     return;
521                 }
522             }
523 
524             mRadioTuner.scan(RadioTuner.DIRECTION_UP, true);
525         }
526 
527         /**
528          * Seeks the radio backwards. To be notified of a successful tune, register as a
529          * {@link android.hardware.radio.RadioTuner.Callback}.
530          */
531         @Override
532         public void seekBackward() {
533             if (mRadioManager == null
534                     || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
535                 return;
536             }
537 
538             if (mRadioTuner == null) {
539                 int radioStatus = openRadioBandInternal(mCurrentRadioBand);
540                 if (radioStatus == RadioManager.STATUS_ERROR) {
541                     return;
542                 }
543             }
544 
545             mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, true);
546         }
547 
548         /**
549          * Mutes the radio.
550          *
551          * @return {@code true} if the mute was successful.
552          */
553         @Override
554         public boolean mute() {
555             if (mRadioManager == null) {
556                 return false;
557             }
558 
559             if (mCarAudioManager == null) {
560                 if (Log.isLoggable(TAG, Log.DEBUG)) {
561                     Log.d(TAG, "mute() called, but not connected to CarAudioManager");
562                 }
563                 return false;
564             }
565 
566             // If the radio does not currently have focus, then no need to do anything because the
567             // radio won't be playing any sound.
568             if (!mHasAudioFocus) {
569                 if (Log.isLoggable(TAG, Log.DEBUG)) {
570                     Log.d(TAG, "mute() called, but radio does not currently have audio focus; "
571                             + "ignoring.");
572                 }
573                 return false;
574             }
575 
576             boolean muteSuccessful = false;
577 
578             try {
579                 muteSuccessful = mCarAudioManager.setMediaMute(true);
580 
581                 if (Log.isLoggable(TAG, Log.DEBUG)) {
582                     Log.d(TAG, "setMediaMute(true) status: " + muteSuccessful);
583                 }
584             } catch (CarNotConnectedException e) {
585                 Log.e(TAG, "mute() failed: " + e.getMessage());
586                 e.printStackTrace();
587             }
588 
589             if (muteSuccessful && mRadioTunerCallbacks.size() > 0) {
590                 for (IRadioCallback callback : mRadioTunerCallbacks) {
591                     try {
592                         callback.onRadioMuteChanged(true);
593                     } catch (RemoteException e) {
594                         Log.e(TAG, "mute() notify failed: " + e.getMessage());
595                     }
596                 }
597             }
598 
599             return muteSuccessful;
600         }
601 
602         /**
603          * Un-mutes the radio and causes audio to play.
604          *
605          * @return {@code true} if the un-mute was successful.
606          */
607         @Override
608         public boolean unMute() {
609             if (mRadioManager == null) {
610                 return false;
611             }
612 
613             if (mCarAudioManager == null) {
614                 if (Log.isLoggable(TAG, Log.DEBUG)) {
615                     Log.d(TAG, "toggleMute() called, but not connected to CarAudioManager");
616                 }
617                 return false;
618             }
619 
620             // Requesting audio focus will automatically un-mute the radio if it had been muted.
621             return requestAudioFocus() == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
622         }
623 
624         /**
625          * Returns {@code true} if the radio is currently muted.
626          */
627         @Override
628         public boolean isMuted() {
629             if (!mHasAudioFocus) {
630                 return true;
631             }
632 
633             if (mRadioManager == null) {
634                 return true;
635             }
636 
637             if (mCarAudioManager == null) {
638                 if (Log.isLoggable(TAG, Log.DEBUG)) {
639                     Log.d(TAG, "isMuted() called, but not connected to CarAudioManager");
640                 }
641                 return true;
642             }
643 
644             boolean isMuted = false;
645 
646             try {
647                 isMuted = mCarAudioManager.isMediaMuted();
648             } catch (CarNotConnectedException e) {
649                 Log.e(TAG, "isMuted() failed: " + e.getMessage());
650                 e.printStackTrace();
651             }
652 
653             return isMuted;
654         }
655 
656         /**
657          * Opens the radio for the given band.
658          *
659          * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
660          *                  {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
661          * @return {@link RadioManager#STATUS_OK} if successful; otherwise,
662          * {@link RadioManager#STATUS_ERROR}.
663          */
664         @Override
665         public int openRadioBand(int radioBand) {
666             if (Log.isLoggable(TAG, Log.DEBUG)) {
667                 Log.d(TAG, "openRadioBand() for band: " + radioBand);
668             }
669 
670             if (mRadioManager == null) {
671                 return RadioManager.STATUS_ERROR;
672             }
673 
674             return openRadioBandInternal(radioBand);
675         }
676 
677         /**
678          * Adds the given {@link android.hardware.radio.RadioTuner.Callback} to be notified
679          * of any radio metadata changes.
680          */
681         @Override
682         public void addRadioTunerCallback(IRadioCallback callback) {
683             if (callback == null) {
684                 return;
685             }
686 
687             mRadioTunerCallbacks.add(callback);
688         }
689 
690         /**
691          * Removes the given {@link android.hardware.radio.RadioTuner.Callback} from receiving
692          * any radio metadata chagnes.
693          */
694         @Override
695         public void removeRadioTunerCallback(IRadioCallback callback) {
696             if (callback == null) {
697                 return;
698             }
699 
700             mRadioTunerCallbacks.remove(callback);
701         }
702 
703         /**
704          * Returns a {@link RadioStation} that encapsulates the information about the current
705          * station the radio is tuned to.
706          */
707         @Override
708         public RadioStation getCurrentRadioStation() {
709             return createCurrentRadioStation();
710         }
711 
712         /**
713          * Returns {@code true} if the radio was able to successfully initialize. A value of
714          * {@code false} here could mean that the {@code RadioService} was not able to connect to
715          * the {@link RadioManager} or there were no radio modules on the current device.
716          */
717         @Override
718         public boolean isInitialized() {
719             return mRadioSuccessfullyInitialized;
720         }
721 
722         /**
723          * Returns {@code true} if the radio currently has focus and is therefore the application
724          * that is supplying music.
725          */
726         @Override
727         public boolean hasFocus() {
728             return mHasAudioFocus;
729         }
730 
731         /**
732          * Returns {@code true} if the current radio module has dual tuners, meaning that a tuner
733          * is available to scan for stations in the background.
734          */
735         @Override
736         public boolean hasDualTuners() {
737             return mModules.size() >= 2;
738         }
739     };
740 
741     /**
742      * A extension of {@link android.hardware.radio.RadioTuner.Callback} that delegates to a
743      * callback registered on this service.
744      */
745     private class InternalRadioCallback extends RadioTuner.Callback {
746         @Override
onProgramInfoChanged(RadioManager.ProgramInfo info)747         public void onProgramInfoChanged(RadioManager.ProgramInfo info) {
748             if (Log.isLoggable(TAG, Log.DEBUG)) {
749                 Log.d(TAG, "onProgramInfoChanged(); info: " + info);
750             }
751 
752             clearMetadata();
753 
754             if (info != null) {
755                 mCurrentRadioChannel = info.getChannel();
756 
757                 if (Log.isLoggable(TAG, Log.DEBUG)) {
758                     Log.d(TAG, "onProgramInfoChanged(); info channel: " + mCurrentRadioChannel);
759                 }
760             }
761 
762             RadioStation station = createCurrentRadioStation();
763 
764             try {
765                 for (IRadioCallback callback : mRadioTunerCallbacks) {
766                     callback.onRadioStationChanged(station);
767                 }
768             } catch (RemoteException e) {
769                 Log.e(TAG, "onProgramInfoChanged(); "
770                         + "Failed to notify IRadioCallbacks: " + e.getMessage());
771             }
772         }
773 
774         @Override
onMetadataChanged(RadioMetadata metadata)775         public void onMetadataChanged(RadioMetadata metadata) {
776             if (Log.isLoggable(TAG, Log.DEBUG)) {
777                 Log.d(TAG, "onMetadataChanged(); metadata: " + metadata);
778             }
779 
780             clearMetadata();
781             updateMetadata(metadata);
782 
783             RadioRds radioRds = createCurrentRadioRds();
784 
785             try {
786                 for (IRadioCallback callback : mRadioTunerCallbacks) {
787                     callback.onRadioMetadataChanged(radioRds);
788                 }
789             } catch (RemoteException e) {
790                 Log.e(TAG, "onMetadataChanged(); "
791                         + "Failed to notify IRadioCallbacks: " + e.getMessage());
792             }
793         }
794 
795         @Override
onConfigurationChanged(RadioManager.BandConfig config)796         public void onConfigurationChanged(RadioManager.BandConfig config) {
797             if (Log.isLoggable(TAG, Log.DEBUG)) {
798                 Log.d(TAG, "onConfigurationChanged(): config: " + config);
799             }
800 
801             clearMetadata();
802 
803             if (config != null) {
804                 mCurrentRadioBand = config.getType();
805 
806                 if (Log.isLoggable(TAG, Log.DEBUG)) {
807                     Log.d(TAG, "onConfigurationChanged(): config type: " + mCurrentRadioBand);
808                 }
809 
810             }
811 
812             try {
813                 for (IRadioCallback callback : mRadioTunerCallbacks) {
814                     callback.onRadioBandChanged(mCurrentRadioBand);
815                 }
816             } catch (RemoteException e) {
817                 Log.e(TAG, "onConfigurationChanged(); "
818                         + "Failed to notify IRadioCallbacks: " + e.getMessage());
819             }
820         }
821 
822         @Override
onError(int status)823         public void onError(int status) {
824             Log.e(TAG, "onError(); status: " + status);
825 
826             // If there is a hardware failure or the radio service died, then this requires a
827             // re-opening of the radio tuner.
828             if (status == RadioTuner.ERROR_HARDWARE_FAILURE
829                     || status == RadioTuner.ERROR_SERVER_DIED) {
830                 if (mRadioTuner != null) {
831                     mRadioTuner.close();
832                     mRadioTuner = null;
833                 }
834 
835                 // Attempt to re-open the RadioTuner. Each time the radio tuner fails to open, the
836                 // mReOpenRadioTunerCount will be incremented.
837                 mHandler.removeCallbacks(mOpenRadioTunerRunnable);
838                 mHandler.postDelayed(mOpenRadioTunerRunnable,
839                         mReOpenRadioTunerCount * RADIO_TUNER_REOPEN_DELAY_MS);
840 
841                 mReOpenRadioTunerCount++;
842             }
843 
844             try {
845                 for (IRadioCallback callback : mRadioTunerCallbacks) {
846                     callback.onError(status);
847                 }
848             } catch (RemoteException e) {
849                 Log.e(TAG, "onError(); Failed to notify IRadioCallbacks: " + e.getMessage());
850             }
851         }
852 
853         @Override
onControlChanged(boolean control)854         public void onControlChanged(boolean control) {
855             // If the radio loses control of the RadioTuner, then close it and allow it to be
856             // re-opened when control has been gained.
857             if (!control) {
858                 close();
859                 return;
860             }
861 
862             if (mRadioTuner == null) {
863                 openRadioBandInternal(mCurrentRadioBand);
864             }
865         }
866 
867         /**
868          * Sets all metadata fields to {@code null}.
869          */
clearMetadata()870         private void clearMetadata() {
871             mCurrentChannelInfo = null;
872             mCurrentArtist = null;
873             mCurrentSongTitle = null;
874         }
875 
876         /**
877          * Retrieves the relevant information off the given {@link RadioMetadata} object and
878          * sets them correspondingly on {@link #mCurrentChannelInfo}, {@link #mCurrentArtist}
879          * and {@link #mCurrentSongTitle}.
880          */
updateMetadata(RadioMetadata metadata)881         private void updateMetadata(RadioMetadata metadata) {
882             if (metadata != null) {
883                 mCurrentChannelInfo = metadata.getString(RadioMetadata.METADATA_KEY_RDS_PS);
884                 mCurrentArtist = metadata.getString(RadioMetadata.METADATA_KEY_ARTIST);
885                 mCurrentSongTitle = metadata.getString(RadioMetadata.METADATA_KEY_TITLE);
886 
887                 if (Log.isLoggable(TAG, Log.DEBUG)) {
888                     Log.d(TAG, String.format("updateMetadata(): [channel info: %s, artist: %s, "
889                             + "song title: %s]", mCurrentChannelInfo, mCurrentArtist,
890                             mCurrentSongTitle));
891                 }
892             }
893         }
894     }
895 
896     private final Runnable mOpenRadioTunerRunnable = () -> openRadioBandInternal(mCurrentRadioBand);
897 }
898