• 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.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
onBind(Intent intent)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
onCreate()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
onDestroy()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 
onPresetsChanged()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      */
openRadioBandInternal(int radioBand)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 
tuneToDefault(int band)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
onPlaybackStateChanged(@laybackStateCompat.State int state)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      */
close()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
onProgramInfoChanged(ProgramInfo info)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
onError(int status)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
onControlChanged(boolean control)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
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)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
onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)481     public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
482         mBrowseTree.loadChildren(parentMediaId, result);
483     }
484 
485     @Override
onStartCommand(Intent intent, int flags, int startId)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
asBinder()498     public IBinder asBinder() {
499         throw new UnsupportedOperationException("Not a binder");
500     }
501 }
502