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