• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2018 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.service;
18 
19 import static com.android.car.radio.util.Remote.tryExec;
20 
21 import android.content.Intent;
22 import android.hardware.radio.ProgramList;
23 import android.hardware.radio.ProgramSelector;
24 import android.hardware.radio.RadioManager.ProgramInfo;
25 import android.hardware.radio.RadioTuner;
26 import android.media.browse.MediaBrowser.MediaItem;
27 import android.media.session.PlaybackState;
28 import android.os.Bundle;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.os.SystemClock;
32 import android.service.media.MediaBrowserService;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.lifecycle.Lifecycle;
37 import androidx.lifecycle.LifecycleOwner;
38 import androidx.lifecycle.LifecycleRegistry;
39 import androidx.lifecycle.LiveData;
40 
41 import com.android.car.broadcastradio.support.Program;
42 import com.android.car.broadcastradio.support.media.BrowseTree;
43 import com.android.car.radio.SkipMode;
44 import com.android.car.radio.audio.AudioStreamController;
45 import com.android.car.radio.bands.ProgramType;
46 import com.android.car.radio.bands.RegionConfig;
47 import com.android.car.radio.media.TunerSession;
48 import com.android.car.radio.platform.ImageMemoryCache;
49 import com.android.car.radio.platform.RadioManagerExt;
50 import com.android.car.radio.platform.RadioTunerExt;
51 import com.android.car.radio.platform.RadioTunerExt.TuneCallback;
52 import com.android.car.radio.storage.RadioStorage;
53 import com.android.car.radio.util.Log;
54 
55 import java.io.FileDescriptor;
56 import java.io.PrintWriter;
57 import java.util.ArrayList;
58 import java.util.HashSet;
59 import java.util.List;
60 import java.util.Objects;
61 
62 /**
63  * A service handling hardware tuner session and audio streaming.
64  */
65 public class RadioAppService extends MediaBrowserService implements LifecycleOwner {
66     private static final String TAG = "BcRadioApp.service";
67 
68     public static String ACTION_APP_SERVICE = "com.android.car.radio.ACTION_APP_SERVICE";
69     private static final long PROGRAM_LIST_RATE_LIMITING = 1000;
70 
71     private final Object mLock = new Object();
72     private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
73     private final List<IRadioAppCallback> mRadioAppCallbacks = new ArrayList<>();
74     private RadioAppServiceWrapper mWrapper;
75 
76     private RadioManagerExt mRadioManager;
77     @Nullable private RadioTunerExt mRadioTuner;
78     @Nullable private ProgramList mProgramList;
79 
80     private RadioStorage mRadioStorage;
81     private ImageMemoryCache mImageCache;
82     @Nullable private AudioStreamController mAudioStreamController;
83 
84     private BrowseTree mBrowseTree;
85     private TunerSession mMediaSession;
86 
87     // current observables state for newly bound IRadioAppCallbacks
88     private ProgramInfo mCurrentProgram = null;
89     private int mCurrentPlaybackState = PlaybackState.STATE_NONE;
90     private long mLastProgramListPush;
91 
92     private RegionConfig mRegionConfigCache;
93 
94     private SkipController mSkipController;
95 
96     @Override
onCreate()97     public void onCreate() {
98         super.onCreate();
99 
100         Log.i(TAG, "Starting RadioAppService...");
101 
102         mWrapper = new RadioAppServiceWrapper(mBinder);
103         mRadioManager = new RadioManagerExt(this);
104         mRadioStorage = RadioStorage.getInstance(this);
105         mImageCache = new ImageMemoryCache(mRadioManager, 1000);
106         mRadioTuner = mRadioManager.openSession(mHardwareCallback, null);
107         if (mRadioTuner == null) {
108             Log.e(TAG, "Couldn't open tuner session");
109             return;
110         }
111 
112         mAudioStreamController = new AudioStreamController(this, mRadioTuner,
113                 this::onPlaybackStateChanged);
114         mBrowseTree = new BrowseTree(this, mImageCache);
115         mMediaSession = new TunerSession(this, mBrowseTree, mWrapper, mImageCache);
116         setSessionToken(mMediaSession.getSessionToken());
117         mBrowseTree.setAmFmRegionConfig(mRadioManager.getAmFmRegionConfig());
118         LiveData<List<Program>> favorites = mRadioStorage.getFavorites();
119         SkipMode skipMode = mRadioStorage.getSkipMode();
120         mSkipController = new SkipController(mBinder, favorites, skipMode);
121         favorites.observe(this, favs -> mBrowseTree.setFavorites(new HashSet<>(favs)));
122 
123         mProgramList = mRadioTuner.getDynamicProgramList(null);
124         if (mProgramList != null) {
125             mBrowseTree.setProgramList(mProgramList);
126             mProgramList.registerListCallback(new ProgramList.ListCallback() {
127                 @Override
128                 public void onItemChanged(@NonNull ProgramSelector.Identifier id) {
129                     onProgramListChanged();
130                 }
131             });
132             mProgramList.addOnCompleteListener(this::pushProgramListUpdate);
133         }
134 
135         tuneToDefault(null);
136         mAudioStreamController.requestMuted(false);
137 
138         mLifecycleRegistry.markState(Lifecycle.State.CREATED);
139     }
140 
141     @Override
onStartCommand(Intent intent, int flags, int startId)142     public int onStartCommand(Intent intent, int flags, int startId) {
143         mLifecycleRegistry.markState(Lifecycle.State.STARTED);
144         if (BrowseTree.ACTION_PLAY_BROADCASTRADIO.equals(intent.getAction())) {
145             Log.i(TAG, "Executing general play radio intent");
146             mMediaSession.getController().getTransportControls().playFromMediaId(
147                     mBrowseTree.getRoot().getRootId(), null);
148             return START_NOT_STICKY;
149         }
150 
151         return super.onStartCommand(intent, flags, startId);
152     }
153 
154     @Override
onBind(Intent intent)155     public IBinder onBind(Intent intent) {
156         mLifecycleRegistry.markState(Lifecycle.State.STARTED);
157         if (mRadioTuner == null) return null;
158         if (ACTION_APP_SERVICE.equals(intent.getAction())) {
159             return mBinder;
160         }
161         return super.onBind(intent);
162     }
163 
164     @Override
onUnbind(Intent intent)165     public boolean onUnbind(Intent intent) {
166         mLifecycleRegistry.markState(Lifecycle.State.CREATED);
167         return false;
168     }
169 
170     @Override
onDestroy()171     public void onDestroy() {
172         Log.i(TAG, "Shutting down RadioAppService...");
173 
174         mLifecycleRegistry.markState(Lifecycle.State.DESTROYED);
175 
176         if (mMediaSession != null) mMediaSession.release();
177         close();
178 
179         super.onDestroy();
180     }
181 
182     @NonNull
183     @Override
getLifecycle()184     public Lifecycle getLifecycle() {
185         return mLifecycleRegistry;
186     }
187 
onPlaybackStateChanged(int newState)188     private void onPlaybackStateChanged(int newState) {
189         synchronized (mLock) {
190             mCurrentPlaybackState = newState;
191             for (IRadioAppCallback callback : mRadioAppCallbacks) {
192                 tryExec(() -> callback.onPlaybackStateChanged(newState));
193             }
194         }
195     }
196 
onProgramListChanged()197     private void onProgramListChanged() {
198         synchronized (mLock) {
199             if (SystemClock.elapsedRealtime() - mLastProgramListPush > PROGRAM_LIST_RATE_LIMITING) {
200                 pushProgramListUpdate();
201             }
202         }
203     }
204 
pushProgramListUpdate()205     private void pushProgramListUpdate() {
206         List<ProgramInfo> plist = mProgramList.toList();
207 
208         synchronized (mLock) {
209             mLastProgramListPush = SystemClock.elapsedRealtime();
210             for (IRadioAppCallback callback : mRadioAppCallbacks) {
211                 tryExec(() -> callback.onProgramListChanged(plist));
212             }
213         }
214     }
215 
tuneToDefault(@ullable ProgramType pt)216     private void tuneToDefault(@Nullable ProgramType pt) {
217         synchronized (mLock) {
218             if (mRadioTuner == null) throw new IllegalStateException("Tuner session is closed");
219             TuneCallback tuneCb = mAudioStreamController.preparePlayback(
220                     AudioStreamController.OPERATION_TUNE);
221             if (tuneCb == null) return;
222 
223             ProgramSelector sel = mRadioStorage.getRecentlySelected(pt);
224             if (sel != null) {
225                 Log.i(TAG, "Restoring recently selected program: " + sel);
226                 try {
227                     mRadioTuner.tune(sel, tuneCb);
228                 } catch (IllegalArgumentException | UnsupportedOperationException e) {
229                     Log.e(TAG, "Can't restore recently selected program: " + sel, e);
230                 }
231                 return;
232             }
233 
234             if (pt == null) pt = ProgramType.FM;
235             Log.i(TAG, "No recently selected program set, selecting default channel for " + pt);
236             pt.tuneToDefault(mRadioTuner, mWrapper.getRegionConfig(), tuneCb);
237         }
238     }
239 
close()240     private void close() {
241         synchronized (mLock) {
242             if (mAudioStreamController != null) {
243                 mAudioStreamController.requestMuted(true);
244                 mAudioStreamController = null;
245             }
246             if (mProgramList != null) {
247                 mProgramList.close();
248                 mProgramList = null;
249             }
250             if (mRadioTuner != null) {
251                 mRadioTuner.close();
252                 mRadioTuner = null;
253             }
254         }
255     }
256 
257     @Override
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)258     public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
259         /* Radio application may restrict who can read its MediaBrowser tree.
260          * Our implementation doesn't.
261          */
262         return mBrowseTree.getRoot();
263     }
264 
265     @Override
onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)266     public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
267         mBrowseTree.loadChildren(parentMediaId, result);
268     }
269 
onHardwareError()270     private void onHardwareError() {
271         close();
272         stopSelf();
273         synchronized (mLock) {
274             for (IRadioAppCallback callback : mRadioAppCallbacks) {
275                 tryExec(() -> callback.onHardwareError());
276             }
277         }
278     }
279 
280     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)281     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
282         if (mSkipController != null) {
283             pw.println("SkipController:"); mSkipController.dump(pw, "  ");
284         } else {
285             pw.println("no SkipController");
286         }
287     }
288 
289     private final IRadioAppService.Stub mBinder = new IRadioAppService.Stub() {
290         @Override
291         public void addCallback(IRadioAppCallback callback) throws RemoteException {
292             synchronized (mLock) {
293                 if (mCurrentProgram != null) callback.onCurrentProgramChanged(mCurrentProgram);
294                 callback.onPlaybackStateChanged(mCurrentPlaybackState);
295                 if (mProgramList != null) callback.onProgramListChanged(mProgramList.toList());
296                 mRadioAppCallbacks.add(callback);
297             }
298         }
299 
300         @Override
301         public void removeCallback(IRadioAppCallback callback) {
302             synchronized (mLock) {
303                 mRadioAppCallbacks.remove(callback);
304             }
305         }
306 
307         @Override
308         public void tune(ProgramSelector sel, ITuneCallback callback) {
309             Objects.requireNonNull(callback);
310             synchronized (mLock) {
311                 if (mRadioTuner == null) throw new IllegalStateException("Tuner session is closed");
312                 TuneCallback tuneCb = mAudioStreamController.preparePlayback(
313                         AudioStreamController.OPERATION_TUNE);
314                 if (tuneCb == null) return;
315                 mRadioTuner.tune(sel, tuneCb.alsoCall(
316                         succ -> tryExec(() -> callback.onFinished(succ))));
317             }
318         }
319 
320         @Override
321         public void seek(boolean forward, ITuneCallback callback) {
322             Objects.requireNonNull(callback);
323             synchronized (mLock) {
324                 if (mRadioTuner == null) throw new IllegalStateException("Tuner session is closed");
325                 TuneCallback tuneCb = mAudioStreamController.preparePlayback(forward
326                         ? AudioStreamController.OPERATION_SEEK_FWD
327                         : AudioStreamController.OPERATION_SEEK_BKW);
328                 if (tuneCb == null) return;
329                 mRadioTuner.seek(forward, tuneCb.alsoCall(
330                         succ -> tryExec(() -> callback.onFinished(succ))));
331             }
332         }
333 
334         @Override
335         public void skip(boolean forward, ITuneCallback callback) throws RemoteException {
336             Objects.requireNonNull(callback);
337 
338             mSkipController.skip(forward, callback);
339         }
340 
341         @Override
342         public void setSkipMode(int mode) {
343             SkipMode newMode = SkipMode.valueOf(mode);
344             if (newMode == null) {
345                 Log.e(TAG, "setSkipMode(): invalid mode " + mode);
346                 return;
347             }
348             mSkipController.setSkipMode(newMode);
349             mRadioStorage.setSkipMode(newMode);
350         }
351 
352         @Override
353         public void step(boolean forward, ITuneCallback callback) {
354             Objects.requireNonNull(callback);
355             synchronized (mLock) {
356                 if (mRadioTuner == null) throw new IllegalStateException("Tuner session is closed");
357                 TuneCallback tuneCb = mAudioStreamController.preparePlayback(forward
358                         ? AudioStreamController.OPERATION_STEP_FWD
359                         : AudioStreamController.OPERATION_STEP_BKW);
360                 if (tuneCb == null) return;
361                 mRadioTuner.step(forward, tuneCb.alsoCall(
362                         succ -> tryExec(() -> callback.onFinished(succ))));
363             }
364         }
365 
366         @Override
367         public void setMuted(boolean muted) {
368             if (mAudioStreamController == null) return;
369             if (muted) mRadioTuner.cancel();
370             mAudioStreamController.requestMuted(muted);
371         }
372 
373         @Override
374         public void switchBand(ProgramType band) {
375             tuneToDefault(band);
376         }
377 
378         @Override
379         public boolean isProgramListSupported() {
380             return mProgramList != null;
381         }
382 
383         @Override
384         public RegionConfig getRegionConfig() {
385             synchronized (mLock) {
386                 if (mRegionConfigCache == null) {
387                     mRegionConfigCache = new RegionConfig(mRadioManager.getAmFmRegionConfig());
388                 }
389                 return mRegionConfigCache;
390             }
391         }
392     };
393 
394     private RadioTuner.Callback mHardwareCallback = new RadioTuner.Callback() {
395         @Override
396         public void onProgramInfoChanged(ProgramInfo info) {
397             Objects.requireNonNull(info);
398 
399             Log.d(TAG, "Program info changed: %s", info);
400 
401             synchronized (mLock) {
402                 mCurrentProgram = info;
403 
404                 /* Storing recently selected program might be limited to explicit tune calls only
405                  * (including next/prev seek), but the implementation would be nontrivial with the
406                  * current API. For now, let's make it simple and make it react to all program
407                  * selector changes. */
408                 mRadioStorage.setRecentlySelected(info.getSelector());
409                 for (IRadioAppCallback callback : mRadioAppCallbacks) {
410                     tryExec(() -> callback.onCurrentProgramChanged(info));
411                 }
412             }
413         }
414 
415         @Override
416         public void onError(int status) {
417             switch (status) {
418                 case RadioTuner.ERROR_HARDWARE_FAILURE:
419                 case RadioTuner.ERROR_SERVER_DIED:
420                     Log.e(TAG, "Fatal hardware error: " + status);
421                     onHardwareError();
422                     break;
423                 default:
424                     Log.w(TAG, "Hardware error: " + status);
425             }
426         }
427 
428         @Override
429         public void onControlChanged(boolean control) {
430             if (!control) onHardwareError();
431         }
432     };
433 }
434