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