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