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