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