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.content.Intent;
     21 import android.hardware.radio.ProgramList;
     22 import android.hardware.radio.ProgramSelector;
     23 import android.hardware.radio.RadioManager;
     24 import android.hardware.radio.RadioManager.ProgramInfo;
     25 import android.hardware.radio.RadioTuner;
     26 import android.os.Bundle;
     27 import android.os.Handler;
     28 import android.os.IBinder;
     29 import android.os.RemoteException;
     30 import android.support.v4.media.MediaBrowserCompat.MediaItem;
     31 import android.support.v4.media.MediaBrowserServiceCompat;
     32 import android.support.v4.media.session.PlaybackStateCompat;
     33 import android.util.Log;
     34 
     35 import com.android.car.broadcastradio.support.Program;
     36 import com.android.car.broadcastradio.support.media.BrowseTree;
     37 import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
     38 import com.android.car.radio.audio.AudioStreamController;
     39 import com.android.car.radio.audio.IPlaybackStateListener;
     40 import com.android.car.radio.media.TunerSession;
     41 import com.android.car.radio.platform.ImageMemoryCache;
     42 import com.android.car.radio.platform.RadioManagerExt;
     43 import com.android.car.radio.service.IRadioCallback;
     44 import com.android.car.radio.service.IRadioManager;
     45 import com.android.car.radio.storage.RadioStorage;
     46 
     47 import java.util.ArrayList;
     48 import java.util.HashSet;
     49 import java.util.List;
     50 import java.util.Objects;
     51 import java.util.Optional;
     52 
     53 /**
     54  * A persistent {@link Service} that is responsible for opening and closing a {@link RadioTuner}.
     55  * All radio operations should be delegated to this class. To be notified of any changes in radio
     56  * metadata, register as a {@link android.hardware.radio.RadioTuner.Callback} on this Service.
     57  *
     58  * <p>Utilize the {@link RadioBinder} to perform radio operations.
     59  */
     60 public class RadioService extends MediaBrowserServiceCompat implements IPlaybackStateListener {
     61 
     62     private static String TAG = "BcRadioApp.uisrv";
     63 
     64     public static String ACTION_UI_SERVICE = "com.android.car.radio.ACTION_UI_SERVICE";
     65 
     66     /**
     67      * The amount of time to wait before re-trying to open the {@link #mRadioTuner}.
     68      */
     69     private static final int RADIO_TUNER_REOPEN_DELAY_MS = 5000;
     70 
     71     private final Object mLock = new Object();
     72 
     73     private int mReOpenRadioTunerCount = 0;
     74     private final Handler mHandler = new Handler();
     75 
     76     private RadioStorage mRadioStorage;
     77     private final RadioStorage.PresetsChangeListener mPresetsListener = this::onPresetsChanged;
     78 
     79     private RadioTuner mRadioTuner;
     80 
     81     private boolean mRadioSuccessfullyInitialized;
     82 
     83     private ProgramInfo mCurrentProgram;
     84 
     85     private RadioManagerExt mRadioManager;
     86     private ImageMemoryCache mImageCache;
     87 
     88     private AudioStreamController mAudioStreamController;
     89 
     90     private BrowseTree mBrowseTree;
     91     private TunerSession mMediaSession;
     92     private ProgramList mProgramList;
     93 
     94     /**
     95      * Whether or not this {@link RadioService} currently has audio focus, meaning it is the
     96      * primary driver of media. Usually, interaction with the radio will be prefaced with an
     97      * explicit request for audio focus. However, this is not ideal when muting the radio, so this
     98      * state needs to be tracked.
     99      */
    100     private boolean mHasAudioFocus;
    101 
    102     /**
    103      * An internal {@link android.hardware.radio.RadioTuner.Callback} that will listen for
    104      * changes in radio metadata and pass these method calls through to
    105      * {@link #mRadioTunerCallbacks}.
    106      */
    107     private RadioTuner.Callback mInternalRadioTunerCallback = new InternalRadioCallback();
    108     private List<IRadioCallback> mRadioTunerCallbacks = new ArrayList<>();
    109 
    110     @Override
    111     public IBinder onBind(Intent intent) {
    112         if (ACTION_UI_SERVICE.equals(intent.getAction())) {
    113             return mBinder;
    114         }
    115         return super.onBind(intent);
    116     }
    117 
    118     @Override
    119     public void onCreate() {
    120         super.onCreate();
    121 
    122         if (Log.isLoggable(TAG, Log.DEBUG)) {
    123             Log.d(TAG, "onCreate()");
    124         }
    125 
    126         mRadioManager = new RadioManagerExt(this);
    127         mAudioStreamController = new AudioStreamController(this, mRadioManager);
    128         mRadioStorage = RadioStorage.getInstance(this);
    129         mImageCache = new ImageMemoryCache(mRadioManager, 1000);
    130 
    131         mBrowseTree = new BrowseTree(this, mImageCache);
    132         mMediaSession = new TunerSession(this, mBrowseTree, mBinder, mImageCache);
    133         setSessionToken(mMediaSession.getSessionToken());
    134         mAudioStreamController.addPlaybackStateListener(mMediaSession);
    135         mBrowseTree.setAmFmRegionConfig(mRadioManager.getAmFmRegionConfig());
    136 
    137         mRadioStorage.addPresetsChangeListener(mPresetsListener);
    138         onPresetsChanged();
    139 
    140         mAudioStreamController.addPlaybackStateListener(this);
    141 
    142         openRadioBandInternal(mRadioStorage.getStoredRadioBand());
    143 
    144         mRadioSuccessfullyInitialized = true;
    145     }
    146 
    147     @Override
    148     public void onDestroy() {
    149         if (Log.isLoggable(TAG, Log.DEBUG)) {
    150             Log.d(TAG, "onDestroy()");
    151         }
    152 
    153         mRadioStorage.removePresetsChangeListener(mPresetsListener);
    154         mMediaSession.release();
    155         mRadioManager.getRadioTunerExt().close();
    156         close();
    157 
    158         super.onDestroy();
    159     }
    160 
    161     private void onPresetsChanged() {
    162         synchronized (mLock) {
    163             mBrowseTree.setFavorites(new HashSet<>(mRadioStorage.getPresets()));
    164             mMediaSession.notifyFavoritesChanged();
    165         }
    166     }
    167 
    168     /**
    169      * Opens the current radio band. Currently, this only supports FM and AM bands.
    170      *
    171      * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
    172      *                  {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
    173      * @return {@link RadioManager#STATUS_OK} if successful; otherwise,
    174      * {@link RadioManager#STATUS_ERROR}.
    175      */
    176     private int openRadioBandInternal(int radioBand) {
    177         if (!mAudioStreamController.requestMuted(false)) return RadioManager.STATUS_ERROR;
    178 
    179         if (mRadioTuner == null) {
    180             mRadioTuner = mRadioManager.openSession(mInternalRadioTunerCallback, null);
    181             mProgramList = mRadioTuner.getDynamicProgramList(null);
    182             mBrowseTree.setProgramList(mProgramList);
    183         }
    184 
    185         if (Log.isLoggable(TAG, Log.DEBUG)) {
    186             Log.d(TAG, "openRadioBandInternal() STATUS_OK");
    187         }
    188 
    189         // Reset the counter for exponential backoff each time the radio tuner has been successfully
    190         // opened.
    191         mReOpenRadioTunerCount = 0;
    192 
    193         tuneToDefault(radioBand);
    194 
    195         return RadioManager.STATUS_OK;
    196     }
    197 
    198     private void tuneToDefault(int band) {
    199         if (!mAudioStreamController.preparePlayback(Optional.empty())) return;
    200 
    201         long storedChannel = mRadioStorage.getStoredRadioChannel(band);
    202         if (storedChannel != RadioStorage.INVALID_RADIO_CHANNEL) {
    203             Log.i(TAG, "Restoring stored program: " + storedChannel);
    204             mRadioTuner.tune(ProgramSelectorExt.createAmFmSelector(storedChannel));
    205         } else {
    206             Log.i(TAG, "No stored program, seeking forward to not play static");
    207 
    208             // TODO(b/80500464): don't hardcode, pull from tuner config
    209             long lastChannel;
    210             if (band == RadioManager.BAND_AM) lastChannel = 1620;
    211             else lastChannel = 108000;
    212             mRadioTuner.tune(ProgramSelectorExt.createAmFmSelector(lastChannel));
    213 
    214             mRadioTuner.scan(RadioTuner.DIRECTION_UP, true);
    215         }
    216     }
    217 
    218     /* TODO(b/73950974): remove onRadioMuteChanged from IRadioCallback,
    219      * use IPlaybackStateListener directly.
    220      */
    221     @Override
    222     public void onPlaybackStateChanged(@PlaybackStateCompat.State int state) {
    223         boolean muted = state != PlaybackStateCompat.STATE_PLAYING;
    224         synchronized (mLock) {
    225             for (IRadioCallback callback : mRadioTunerCallbacks) {
    226                 try {
    227                     callback.onRadioMuteChanged(muted);
    228                 } catch (RemoteException e) {
    229                     Log.e(TAG, "Mute state change callback failed", e);
    230                 }
    231             }
    232         }
    233     }
    234 
    235     /**
    236      * Closes any active {@link RadioTuner}s and releases audio focus.
    237      */
    238     private void close() {
    239         if (Log.isLoggable(TAG, Log.DEBUG)) {
    240             Log.d(TAG, "close()");
    241         }
    242 
    243         mAudioStreamController.requestMuted(true);
    244 
    245         if (mProgramList != null) {
    246             mProgramList.close();
    247             mProgramList = null;
    248         }
    249         if (mRadioTuner != null) {
    250             mRadioTuner.close();
    251             mRadioTuner = null;
    252         }
    253     }
    254 
    255     private IRadioManager.Stub mBinder = new IRadioManager.Stub() {
    256         /**
    257          * Tunes the radio to the given frequency. To be notified of a successful tune, register
    258          * as a {@link android.hardware.radio.RadioTuner.Callback}.
    259          */
    260         @Override
    261         public void tune(ProgramSelector sel) {
    262             if (!mAudioStreamController.preparePlayback(Optional.empty())) return;
    263             mRadioTuner.tune(sel);
    264         }
    265 
    266         @Override
    267         public List<ProgramInfo> getProgramList() {
    268             return mRadioTuner.getDynamicProgramList(null).toList();
    269         }
    270 
    271         /**
    272          * Seeks the radio forward. To be notified of a successful tune, register as a
    273          * {@link android.hardware.radio.RadioTuner.Callback}.
    274          */
    275         @Override
    276         public void seekForward() {
    277             if (!mAudioStreamController.preparePlayback(Optional.of(true))) return;
    278 
    279             if (mRadioTuner == null) {
    280                 int radioStatus = openRadioBandInternal(mRadioStorage.getStoredRadioBand());
    281                 if (radioStatus == RadioManager.STATUS_ERROR) {
    282                     return;
    283                 }
    284             }
    285 
    286             mRadioTuner.scan(RadioTuner.DIRECTION_UP, true);
    287         }
    288 
    289         /**
    290          * Seeks the radio backwards. To be notified of a successful tune, register as a
    291          * {@link android.hardware.radio.RadioTuner.Callback}.
    292          */
    293         @Override
    294         public void seekBackward() {
    295             if (!mAudioStreamController.preparePlayback(Optional.of(false))) return;
    296 
    297             if (mRadioTuner == null) {
    298                 int radioStatus = openRadioBandInternal(mRadioStorage.getStoredRadioBand());
    299                 if (radioStatus == RadioManager.STATUS_ERROR) {
    300                     return;
    301                 }
    302             }
    303 
    304             mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, true);
    305         }
    306 
    307         /**
    308          * Mutes the radio.
    309          *
    310          * @return {@code true} if the mute was successful.
    311          */
    312         @Override
    313         public boolean mute() {
    314             return mAudioStreamController.requestMuted(true);
    315         }
    316 
    317         /**
    318          * Un-mutes the radio and causes audio to play.
    319          *
    320          * @return {@code true} if the un-mute was successful.
    321          */
    322         @Override
    323         public boolean unMute() {
    324             return mAudioStreamController.requestMuted(false);
    325         }
    326 
    327         /**
    328          * Returns {@code true} if the radio is currently muted.
    329          */
    330         @Override
    331         public boolean isMuted() {
    332             return mAudioStreamController.isMuted();
    333         }
    334 
    335         @Override
    336         public void addFavorite(Program program) {
    337             mRadioStorage.storePreset(program);
    338         }
    339 
    340         @Override
    341         public void removeFavorite(ProgramSelector sel) {
    342             mRadioStorage.removePreset(sel);
    343         }
    344 
    345         @Override
    346         public void switchBand(int radioBand) {
    347             tuneToDefault(radioBand);
    348         }
    349 
    350         /**
    351          * Adds the given {@link android.hardware.radio.RadioTuner.Callback} to be notified
    352          * of any radio metadata changes.
    353          */
    354         @Override
    355         public void addRadioTunerCallback(IRadioCallback callback) {
    356             if (callback == null) {
    357                 return;
    358             }
    359 
    360             mRadioTunerCallbacks.add(callback);
    361         }
    362 
    363         /**
    364          * Removes the given {@link android.hardware.radio.RadioTuner.Callback} from receiving
    365          * any radio metadata chagnes.
    366          */
    367         @Override
    368         public void removeRadioTunerCallback(IRadioCallback callback) {
    369             if (callback == null) {
    370                 return;
    371             }
    372 
    373             mRadioTunerCallbacks.remove(callback);
    374         }
    375 
    376         @Override
    377         public ProgramInfo getCurrentProgramInfo() {
    378             return mCurrentProgram;
    379         }
    380 
    381         /**
    382          * Returns {@code true} if the radio was able to successfully initialize. A value of
    383          * {@code false} here could mean that the {@code RadioService} was not able to connect to
    384          * the {@link RadioManager} or there were no radio modules on the current device.
    385          */
    386         @Override
    387         public boolean isInitialized() {
    388             return mRadioSuccessfullyInitialized;
    389         }
    390 
    391         /**
    392          * Returns {@code true} if the radio currently has focus and is therefore the application
    393          * that is supplying music.
    394          */
    395         @Override
    396         public boolean hasFocus() {
    397             return mHasAudioFocus;
    398         }
    399     };
    400 
    401     /**
    402      * A extension of {@link android.hardware.radio.RadioTuner.Callback} that delegates to a
    403      * callback registered on this service.
    404      */
    405     private class InternalRadioCallback extends RadioTuner.Callback {
    406         @Override
    407         public void onProgramInfoChanged(ProgramInfo info) {
    408             if (Log.isLoggable(TAG, Log.DEBUG)) {
    409                 Log.d(TAG, "Program info changed: " + info);
    410             }
    411 
    412             mCurrentProgram = Objects.requireNonNull(info);
    413             mMediaSession.notifyProgramInfoChanged(info);
    414             mAudioStreamController.notifyProgramInfoChanged();
    415             mRadioStorage.storeRadioChannel(info.getSelector());
    416 
    417             for (IRadioCallback callback : mRadioTunerCallbacks) {
    418                 try {
    419                     callback.onCurrentProgramInfoChanged(info);
    420                 } catch (RemoteException e) {
    421                     Log.e(TAG, "Failed to notify about changed radio station", e);
    422                 }
    423             }
    424         }
    425 
    426         @Override
    427         public void onError(int status) {
    428             Log.e(TAG, "onError(); status: " + status);
    429 
    430             // If there is a hardware failure or the radio service died, then this requires a
    431             // re-opening of the radio tuner.
    432             if (status == RadioTuner.ERROR_HARDWARE_FAILURE
    433                     || status == RadioTuner.ERROR_SERVER_DIED) {
    434                 close();
    435 
    436                 // Attempt to re-open the RadioTuner. Each time the radio tuner fails to open, the
    437                 // mReOpenRadioTunerCount will be incremented.
    438                 mHandler.removeCallbacks(mOpenRadioTunerRunnable);
    439                 mHandler.postDelayed(mOpenRadioTunerRunnable,
    440                         mReOpenRadioTunerCount * RADIO_TUNER_REOPEN_DELAY_MS);
    441 
    442                 mReOpenRadioTunerCount++;
    443             }
    444 
    445             try {
    446                 for (IRadioCallback callback : mRadioTunerCallbacks) {
    447                     callback.onError(status);
    448                 }
    449             } catch (RemoteException e) {
    450                 Log.e(TAG, "onError(); Failed to notify IRadioCallbacks: " + e.getMessage());
    451             }
    452         }
    453 
    454         @Override
    455         public void onControlChanged(boolean control) {
    456             // If the radio loses control of the RadioTuner, then close it and allow it to be
    457             // re-opened when control has been gained.
    458             if (!control) {
    459                 close();
    460                 return;
    461             }
    462 
    463             if (mRadioTuner == null) {
    464                 openRadioBandInternal(mRadioStorage.getStoredRadioBand());
    465             }
    466         }
    467     }
    468 
    469     private final Runnable mOpenRadioTunerRunnable =
    470             () -> openRadioBandInternal(mRadioStorage.getStoredRadioBand());
    471 
    472     @Override
    473     public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
    474         /* Radio application may restrict who can read its MediaBrowser tree.
    475          * Our implementation doesn't.
    476          */
    477         return mBrowseTree.getRoot();
    478     }
    479 
    480     @Override
    481     public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
    482         mBrowseTree.loadChildren(parentMediaId, result);
    483     }
    484 
    485     @Override
    486     public int onStartCommand(Intent intent, int flags, int startId) {
    487         if (BrowseTree.ACTION_PLAY_BROADCASTRADIO.equals(intent.getAction())) {
    488             Log.i(TAG, "Executing general play radio intent");
    489             mMediaSession.getController().getTransportControls().playFromMediaId(
    490                     mBrowseTree.getRoot().getRootId(), null);
    491             return START_NOT_STICKY;
    492         }
    493 
    494         return super.onStartCommand(intent, flags, startId);
    495     }
    496 
    497     @Override
    498     public IBinder asBinder() {
    499         throw new UnsupportedOperationException("Not a binder");
    500     }
    501 }
    502