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