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.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
    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
    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 RadioDemo
    141      */
    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      */
    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.isFmBand()) {
    189                 mFmDescriptor = (RadioManager.FmBandDescriptor) band;
    190             }
    191 
    192             if (mAmDescriptor == null && band.isAmBand()) {
    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         // TODO(b/63101896): we don't know if the second tuner is for the same medium, so we don't
    212         // set background scanner for now.
    213 
    214         mRadioSuccessfullyInitialized = true;
    215     }
    216 
    217     @Override
    218     public void onDestroy() {
    219         if (Log.isLoggable(TAG, Log.DEBUG)) {
    220             Log.d(TAG, "onDestroy()");
    221         }
    222 
    223         close();
    224 
    225         if (mCarApi != null) {
    226             mCarApi.disconnect();
    227         }
    228 
    229         super.onDestroy();
    230     }
    231 
    232     /**
    233      * Opens the current radio band. Currently, this only supports FM and AM bands.
    234      *
    235      * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
    236      *                  {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
    237      * @return {@link RadioManager#STATUS_OK} if successful; otherwise,
    238      * {@link RadioManager#STATUS_ERROR}.
    239      */
    240     private int openRadioBandInternal(int radioBand) {
    241         if (requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    242             Log.e(TAG, "openRadioBandInternal() audio focus request fail");
    243             return RadioManager.STATUS_ERROR;
    244         }
    245 
    246         mCurrentRadioBand = radioBand;
    247         RadioManager.BandConfig config = getRadioConfig(radioBand);
    248 
    249         if (config == null) {
    250             Log.w(TAG, "Cannot create config for radio band: " + radioBand);
    251             return RadioManager.STATUS_ERROR;
    252         }
    253 
    254         if (mRadioTuner != null) {
    255             mRadioTuner.setConfiguration(config);
    256         } else {
    257             mRadioTuner = mRadioManager.openTuner(mModules.get(0).getId(), config, true,
    258                     mInternalRadioTunerCallback, null /* handler */);
    259         }
    260 
    261         if (Log.isLoggable(TAG, Log.DEBUG)) {
    262             Log.d(TAG, "openRadioBandInternal() STATUS_OK");
    263         }
    264 
    265         if (mBackgroundScanner != null) {
    266             mBackgroundScanner.onRadioBandChanged(radioBand);
    267         }
    268 
    269         // Reset the counter for exponential backoff each time the radio tuner has been successfully
    270         // opened.
    271         mReOpenRadioTunerCount = 0;
    272 
    273         return RadioManager.STATUS_OK;
    274     }
    275 
    276     /**
    277      * Returns a {@link RadioRds} object that holds all the current radio metadata. If all the
    278      * metadata is empty, then {@code null} is returned.
    279      */
    280     @Nullable
    281     private RadioRds createCurrentRadioRds() {
    282         if (TextUtils.isEmpty(mCurrentChannelInfo) && TextUtils.isEmpty(mCurrentArtist)
    283                 && TextUtils.isEmpty(mCurrentSongTitle)) {
    284             return null;
    285         }
    286 
    287         return new RadioRds(mCurrentChannelInfo, mCurrentArtist, mCurrentSongTitle);
    288     }
    289 
    290     /**
    291      * Creates a {@link RadioStation} that encapsulates all the information about the current
    292      * radio station.
    293      */
    294     private RadioStation createCurrentRadioStation() {
    295         // mCurrentRadioChannel can possibly be invalid if this class never receives a callback
    296         // for onProgramInfoChanged(). As a result, manually retrieve the information for the
    297         // current station from RadioTuner if this is the case.
    298         if (mCurrentRadioChannel == RadioStorage.INVALID_RADIO_CHANNEL && mRadioTuner != null) {
    299             if (Log.isLoggable(TAG, Log.DEBUG)) {
    300                 Log.d(TAG, "createCurrentRadioStation(); invalid current radio channel. "
    301                         + "Calling getProgramInformation for valid station");
    302             }
    303 
    304             // getProgramInformation() expects an array of size 1.
    305             RadioManager.ProgramInfo[] info = new RadioManager.ProgramInfo[1];
    306             int status = mRadioTuner.getProgramInformation(info);
    307 
    308             if (Log.isLoggable(TAG, Log.DEBUG)) {
    309                 Log.d(TAG, "getProgramInformation() status: " + status + "; info: " + info[0]);
    310             }
    311 
    312             if (status == RadioManager.STATUS_OK && info[0] != null) {
    313                 mCurrentRadioChannel = info[0].getChannel();
    314 
    315                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    316                     Log.d(TAG, "program info channel: " + mCurrentRadioChannel);
    317                 }
    318             }
    319         }
    320 
    321         return new RadioStation(mCurrentRadioChannel, 0 /* subChannelNumber */,
    322                 mCurrentRadioBand, createCurrentRadioRds());
    323     }
    324 
    325     /**
    326      * Returns the proper {@link android.hardware.radio.RadioManager.BandConfig} for the given
    327      * radio band. {@code null} is returned if the band is not suppored.
    328      */
    329     @Nullable
    330     private RadioManager.BandConfig getRadioConfig(int selectedRadioBand) {
    331         switch (selectedRadioBand) {
    332             case RadioManager.BAND_AM:
    333             case RadioManager.BAND_AM_HD:
    334                 return mAmConfig;
    335             case RadioManager.BAND_FM:
    336             case RadioManager.BAND_FM_HD:
    337                 return mFmConfig;
    338 
    339             default:
    340                 return null;
    341         }
    342     }
    343 
    344     private int requestAudioFocus() {
    345         int status = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
    346         try {
    347             status = mCarAudioManager.requestAudioFocus(this, mRadioAudioAttributes,
    348                     AudioManager.AUDIOFOCUS_GAIN, 0);
    349         } catch (CarNotConnectedException e) {
    350             Log.e(TAG, "requestAudioFocus() failed", e);
    351         }
    352 
    353         if (Log.isLoggable(TAG, Log.DEBUG)) {
    354             Log.d(TAG, "requestAudioFocus status: " + status);
    355         }
    356 
    357         if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    358             mHasAudioFocus = true;
    359 
    360             // Receiving audio focus means that the radio is un-muted.
    361             for (IRadioCallback callback : mRadioTunerCallbacks) {
    362                 try {
    363                     callback.onRadioMuteChanged(false);
    364                 } catch (RemoteException e) {
    365                     Log.e(TAG, "requestAudioFocus(); onRadioMuteChanged() notify failed: "
    366                             + e.getMessage());
    367                 }
    368             }
    369         }
    370 
    371         return status;
    372     }
    373 
    374     private void abandonAudioFocus() {
    375         if (Log.isLoggable(TAG, Log.DEBUG)) {
    376             Log.d(TAG, "abandonAudioFocus()");
    377         }
    378 
    379         if (mCarAudioManager == null) {
    380             return;
    381         }
    382 
    383         mCarAudioManager.abandonAudioFocus(this, mRadioAudioAttributes);
    384         mHasAudioFocus = false;
    385 
    386         for (IRadioCallback callback : mRadioTunerCallbacks) {
    387             try {
    388                 callback.onRadioMuteChanged(true);
    389             } catch (RemoteException e) {
    390                 Log.e(TAG, "abandonAudioFocus(); onRadioMutechanged() notify failed: "
    391                         + e.getMessage());
    392             }
    393         }
    394     }
    395 
    396     /**
    397      * Closes any active {@link RadioTuner}s and releases audio focus.
    398      */
    399     private void close() {
    400         if (Log.isLoggable(TAG, Log.DEBUG)) {
    401             Log.d(TAG, "close()");
    402         }
    403 
    404         abandonAudioFocus();
    405 
    406         if (mRadioTuner != null) {
    407             mRadioTuner.close();
    408             mRadioTuner = null;
    409         }
    410     }
    411 
    412     @Override
    413     public void onAudioFocusChange(int focusChange) {
    414         if (Log.isLoggable(TAG, Log.DEBUG)) {
    415             Log.d(TAG, "focus change: " + focusChange);
    416         }
    417 
    418         switch (focusChange) {
    419             case AudioManager.AUDIOFOCUS_GAIN:
    420                 mHasAudioFocus = true;
    421                 openRadioBandInternal(mCurrentRadioBand);
    422                 break;
    423 
    424             // For a transient loss, just allow the focus to be released. The radio will stop
    425             // itself automatically. There is no need for an explicit abandon audio focus call
    426             // because this removes the AudioFocusChangeListener.
    427             case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
    428             case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
    429                 mHasAudioFocus = false;
    430                 break;
    431 
    432             case AudioManager.AUDIOFOCUS_LOSS:
    433                 close();
    434                 break;
    435 
    436             default:
    437                 // Do nothing for all other cases.
    438         }
    439     }
    440 
    441     /**
    442      * {@link CarConnectionCallback} that retrieves the {@link CarRadioManager}.
    443      */
    444     private final CarConnectionCallback mCarConnectionCallback =
    445             new CarConnectionCallback() {
    446                 @Override
    447                 public void onConnected(Car car) {
    448                     if (Log.isLoggable(TAG, Log.DEBUG)) {
    449                         Log.d(TAG, "Car service connected.");
    450                     }
    451                     try {
    452                         // The CarAudioManager only needs to be retrieved once.
    453                         if (mCarAudioManager == null) {
    454                             mCarAudioManager = (CarAudioManager) mCarApi.getCarManager(
    455                                     android.car.Car.AUDIO_SERVICE);
    456 
    457                             mRadioAudioAttributes = mCarAudioManager.getAudioAttributesForCarUsage(
    458                                     CarAudioManager.CAR_AUDIO_USAGE_RADIO);
    459                         }
    460                     } catch (CarNotConnectedException e) {
    461                         //TODO finish
    462                         Log.e(TAG, "Car not connected");
    463                     }
    464                 }
    465 
    466                 @Override
    467                 public void onDisconnected(Car car) {
    468                     if (Log.isLoggable(TAG, Log.DEBUG)) {
    469                         Log.d(TAG, "Car service disconnected.");
    470                     }
    471                 }
    472             };
    473 
    474     private IRadioManager.Stub mBinder = new IRadioManager.Stub() {
    475         /**
    476          * Tunes the radio to the given frequency. To be notified of a successful tune, register
    477          * as a {@link android.hardware.radio.RadioTuner.Callback}.
    478          */
    479         @Override
    480         public void tune(RadioStation radioStation) {
    481             if (mRadioManager == null || radioStation == null
    482                     || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    483                 return;
    484             }
    485 
    486             if (mRadioTuner == null || radioStation.getRadioBand() != mCurrentRadioBand) {
    487                 int radioStatus = openRadioBandInternal(radioStation.getRadioBand());
    488                 if (radioStatus == RadioManager.STATUS_ERROR) {
    489                     return;
    490                 }
    491             }
    492 
    493             int status = mRadioTuner.tune(radioStation.getChannelNumber(), 0 /* subChannel */);
    494 
    495             if (Log.isLoggable(TAG, Log.DEBUG)) {
    496                 Log.d(TAG, "Tuning to station: " + radioStation + "\n\tstatus: " + status);
    497             }
    498         }
    499 
    500         /**
    501          * Seeks the radio forward. To be notified of a successful tune, register as a
    502          * {@link android.hardware.radio.RadioTuner.Callback}.
    503          */
    504         @Override
    505         public void seekForward() {
    506             if (mRadioManager == null
    507                     || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    508                 return;
    509             }
    510 
    511             if (mRadioTuner == null) {
    512                 int radioStatus = openRadioBandInternal(mCurrentRadioBand);
    513                 if (radioStatus == RadioManager.STATUS_ERROR) {
    514                     return;
    515                 }
    516             }
    517 
    518             mRadioTuner.scan(RadioTuner.DIRECTION_UP, true);
    519         }
    520 
    521         /**
    522          * Seeks the radio backwards. To be notified of a successful tune, register as a
    523          * {@link android.hardware.radio.RadioTuner.Callback}.
    524          */
    525         @Override
    526         public void seekBackward() {
    527             if (mRadioManager == null
    528                     || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    529                 return;
    530             }
    531 
    532             if (mRadioTuner == null) {
    533                 int radioStatus = openRadioBandInternal(mCurrentRadioBand);
    534                 if (radioStatus == RadioManager.STATUS_ERROR) {
    535                     return;
    536                 }
    537             }
    538 
    539             mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, true);
    540         }
    541 
    542         /**
    543          * Mutes the radio.
    544          *
    545          * @return {@code true} if the mute was successful.
    546          */
    547         @Override
    548         public boolean mute() {
    549             if (mRadioManager == null) {
    550                 return false;
    551             }
    552 
    553             if (mCarAudioManager == null) {
    554                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    555                     Log.d(TAG, "mute() called, but not connected to CarAudioManager");
    556                 }
    557                 return false;
    558             }
    559 
    560             // If the radio does not currently have focus, then no need to do anything because the
    561             // radio won't be playing any sound.
    562             if (!mHasAudioFocus) {
    563                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    564                     Log.d(TAG, "mute() called, but radio does not currently have audio focus; "
    565                             + "ignoring.");
    566                 }
    567                 return false;
    568             }
    569 
    570             boolean muteSuccessful = false;
    571 
    572             try {
    573                 muteSuccessful = mCarAudioManager.setMediaMute(true);
    574 
    575                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    576                     Log.d(TAG, "setMediaMute(true) status: " + muteSuccessful);
    577                 }
    578             } catch (CarNotConnectedException e) {
    579                 Log.e(TAG, "mute() failed: " + e.getMessage());
    580                 e.printStackTrace();
    581             }
    582 
    583             if (muteSuccessful && mRadioTunerCallbacks.size() > 0) {
    584                 for (IRadioCallback callback : mRadioTunerCallbacks) {
    585                     try {
    586                         callback.onRadioMuteChanged(true);
    587                     } catch (RemoteException e) {
    588                         Log.e(TAG, "mute() notify failed: " + e.getMessage());
    589                     }
    590                 }
    591             }
    592 
    593             return muteSuccessful;
    594         }
    595 
    596         /**
    597          * Un-mutes the radio and causes audio to play.
    598          *
    599          * @return {@code true} if the un-mute was successful.
    600          */
    601         @Override
    602         public boolean unMute() {
    603             if (mRadioManager == null) {
    604                 return false;
    605             }
    606 
    607             if (mCarAudioManager == null) {
    608                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    609                     Log.d(TAG, "toggleMute() called, but not connected to CarAudioManager");
    610                 }
    611                 return false;
    612             }
    613 
    614             // Requesting audio focus will automatically un-mute the radio if it had been muted.
    615             return requestAudioFocus() == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
    616         }
    617 
    618         /**
    619          * Returns {@code true} if the radio is currently muted.
    620          */
    621         @Override
    622         public boolean isMuted() {
    623             if (!mHasAudioFocus) {
    624                 return true;
    625             }
    626 
    627             if (mRadioManager == null) {
    628                 return true;
    629             }
    630 
    631             if (mCarAudioManager == null) {
    632                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    633                     Log.d(TAG, "isMuted() called, but not connected to CarAudioManager");
    634                 }
    635                 return true;
    636             }
    637 
    638             boolean isMuted = false;
    639 
    640             try {
    641                 isMuted = mCarAudioManager.isMediaMuted();
    642             } catch (CarNotConnectedException e) {
    643                 Log.e(TAG, "isMuted() failed: " + e.getMessage());
    644                 e.printStackTrace();
    645             }
    646 
    647             return isMuted;
    648         }
    649 
    650         /**
    651          * Opens the radio for the given band.
    652          *
    653          * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
    654          *                  {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
    655          * @return {@link RadioManager#STATUS_OK} if successful; otherwise,
    656          * {@link RadioManager#STATUS_ERROR}.
    657          */
    658         @Override
    659         public int openRadioBand(int radioBand) {
    660             if (Log.isLoggable(TAG, Log.DEBUG)) {
    661                 Log.d(TAG, "openRadioBand() for band: " + radioBand);
    662             }
    663 
    664             if (mRadioManager == null) {
    665                 return RadioManager.STATUS_ERROR;
    666             }
    667 
    668             return openRadioBandInternal(radioBand);
    669         }
    670 
    671         /**
    672          * Adds the given {@link android.hardware.radio.RadioTuner.Callback} to be notified
    673          * of any radio metadata changes.
    674          */
    675         @Override
    676         public void addRadioTunerCallback(IRadioCallback callback) {
    677             if (callback == null) {
    678                 return;
    679             }
    680 
    681             mRadioTunerCallbacks.add(callback);
    682         }
    683 
    684         /**
    685          * Removes the given {@link android.hardware.radio.RadioTuner.Callback} from receiving
    686          * any radio metadata chagnes.
    687          */
    688         @Override
    689         public void removeRadioTunerCallback(IRadioCallback callback) {
    690             if (callback == null) {
    691                 return;
    692             }
    693 
    694             mRadioTunerCallbacks.remove(callback);
    695         }
    696 
    697         /**
    698          * Returns a {@link RadioStation} that encapsulates the information about the current
    699          * station the radio is tuned to.
    700          */
    701         @Override
    702         public RadioStation getCurrentRadioStation() {
    703             return createCurrentRadioStation();
    704         }
    705 
    706         /**
    707          * Returns {@code true} if the radio was able to successfully initialize. A value of
    708          * {@code false} here could mean that the {@code RadioService} was not able to connect to
    709          * the {@link RadioManager} or there were no radio modules on the current device.
    710          */
    711         @Override
    712         public boolean isInitialized() {
    713             return mRadioSuccessfullyInitialized;
    714         }
    715 
    716         /**
    717          * Returns {@code true} if the radio currently has focus and is therefore the application
    718          * that is supplying music.
    719          */
    720         @Override
    721         public boolean hasFocus() {
    722             return mHasAudioFocus;
    723         }
    724 
    725         /**
    726          * Returns {@code true} if the current radio module has dual tuners, meaning that a tuner
    727          * is available to scan for stations in the background.
    728          */
    729         @Override
    730         public boolean hasDualTuners() {
    731             return mModules.size() >= 2;
    732         }
    733     };
    734 
    735     /**
    736      * A extension of {@link android.hardware.radio.RadioTuner.Callback} that delegates to a
    737      * callback registered on this service.
    738      */
    739     private class InternalRadioCallback extends RadioTuner.Callback {
    740         @Override
    741         public void onProgramInfoChanged(RadioManager.ProgramInfo info) {
    742             if (Log.isLoggable(TAG, Log.DEBUG)) {
    743                 Log.d(TAG, "onProgramInfoChanged(); info: " + info);
    744             }
    745 
    746             clearMetadata();
    747 
    748             if (info != null) {
    749                 mCurrentRadioChannel = info.getChannel();
    750 
    751                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    752                     Log.d(TAG, "onProgramInfoChanged(); info channel: " + mCurrentRadioChannel);
    753                 }
    754             }
    755 
    756             RadioStation station = createCurrentRadioStation();
    757 
    758             try {
    759                 for (IRadioCallback callback : mRadioTunerCallbacks) {
    760                     callback.onRadioStationChanged(station);
    761                 }
    762             } catch (RemoteException e) {
    763                 Log.e(TAG, "onProgramInfoChanged(); "
    764                         + "Failed to notify IRadioCallbacks: " + e.getMessage());
    765             }
    766         }
    767 
    768         @Override
    769         public void onMetadataChanged(RadioMetadata metadata) {
    770             if (Log.isLoggable(TAG, Log.DEBUG)) {
    771                 Log.d(TAG, "onMetadataChanged(); metadata: " + metadata);
    772             }
    773 
    774             clearMetadata();
    775             updateMetadata(metadata);
    776 
    777             RadioRds radioRds = createCurrentRadioRds();
    778 
    779             try {
    780                 for (IRadioCallback callback : mRadioTunerCallbacks) {
    781                     callback.onRadioMetadataChanged(radioRds);
    782                 }
    783             } catch (RemoteException e) {
    784                 Log.e(TAG, "onMetadataChanged(); "
    785                         + "Failed to notify IRadioCallbacks: " + e.getMessage());
    786             }
    787         }
    788 
    789         @Override
    790         public void onConfigurationChanged(RadioManager.BandConfig config) {
    791             if (Log.isLoggable(TAG, Log.DEBUG)) {
    792                 Log.d(TAG, "onConfigurationChanged(): config: " + config);
    793             }
    794 
    795             clearMetadata();
    796 
    797             if (config != null) {
    798                 mCurrentRadioBand = config.getType();
    799 
    800                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    801                     Log.d(TAG, "onConfigurationChanged(): config type: " + mCurrentRadioBand);
    802                 }
    803 
    804             }
    805 
    806             try {
    807                 for (IRadioCallback callback : mRadioTunerCallbacks) {
    808                     callback.onRadioBandChanged(mCurrentRadioBand);
    809                 }
    810             } catch (RemoteException e) {
    811                 Log.e(TAG, "onConfigurationChanged(); "
    812                         + "Failed to notify IRadioCallbacks: " + e.getMessage());
    813             }
    814         }
    815 
    816         @Override
    817         public void onError(int status) {
    818             Log.e(TAG, "onError(); status: " + status);
    819 
    820             // If there is a hardware failure or the radio service died, then this requires a
    821             // re-opening of the radio tuner.
    822             if (status == RadioTuner.ERROR_HARDWARE_FAILURE
    823                     || status == RadioTuner.ERROR_SERVER_DIED) {
    824                 if (mRadioTuner != null) {
    825                     mRadioTuner.close();
    826                     mRadioTuner = null;
    827                 }
    828 
    829                 // Attempt to re-open the RadioTuner. Each time the radio tuner fails to open, the
    830                 // mReOpenRadioTunerCount will be incremented.
    831                 mHandler.removeCallbacks(mOpenRadioTunerRunnable);
    832                 mHandler.postDelayed(mOpenRadioTunerRunnable,
    833                         mReOpenRadioTunerCount * RADIO_TUNER_REOPEN_DELAY_MS);
    834 
    835                 mReOpenRadioTunerCount++;
    836             }
    837 
    838             try {
    839                 for (IRadioCallback callback : mRadioTunerCallbacks) {
    840                     callback.onError(status);
    841                 }
    842             } catch (RemoteException e) {
    843                 Log.e(TAG, "onError(); Failed to notify IRadioCallbacks: " + e.getMessage());
    844             }
    845         }
    846 
    847         @Override
    848         public void onControlChanged(boolean control) {
    849             // If the radio loses control of the RadioTuner, then close it and allow it to be
    850             // re-opened when control has been gained.
    851             if (!control) {
    852                 close();
    853                 return;
    854             }
    855 
    856             if (mRadioTuner == null) {
    857                 openRadioBandInternal(mCurrentRadioBand);
    858             }
    859         }
    860 
    861         /**
    862          * Sets all metadata fields to {@code null}.
    863          */
    864         private void clearMetadata() {
    865             mCurrentChannelInfo = null;
    866             mCurrentArtist = null;
    867             mCurrentSongTitle = null;
    868         }
    869 
    870         /**
    871          * Retrieves the relevant information off the given {@link RadioMetadata} object and
    872          * sets them correspondingly on {@link #mCurrentChannelInfo}, {@link #mCurrentArtist}
    873          * and {@link #mCurrentSongTitle}.
    874          */
    875         private void updateMetadata(RadioMetadata metadata) {
    876             if (metadata != null) {
    877                 mCurrentChannelInfo = metadata.getString(RadioMetadata.METADATA_KEY_RDS_PS);
    878                 mCurrentArtist = metadata.getString(RadioMetadata.METADATA_KEY_ARTIST);
    879                 mCurrentSongTitle = metadata.getString(RadioMetadata.METADATA_KEY_TITLE);
    880 
    881                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    882                     Log.d(TAG, String.format("updateMetadata(): [channel info: %s, artist: %s, "
    883                             + "song title: %s]", mCurrentChannelInfo, mCurrentArtist,
    884                             mCurrentSongTitle));
    885                 }
    886             }
    887         }
    888     }
    889 
    890     private final Runnable mOpenRadioTunerRunnable = () -> openRadioBandInternal(mCurrentRadioBand);
    891 }
    892