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