1 /* 2 * Copyright (C) 2016 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; 18 19 import android.app.Service; 20 import android.content.Intent; 21 import android.hardware.radio.ProgramList; 22 import android.hardware.radio.ProgramSelector; 23 import android.hardware.radio.RadioManager; 24 import android.hardware.radio.RadioManager.ProgramInfo; 25 import android.hardware.radio.RadioTuner; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.IBinder; 29 import android.os.RemoteException; 30 import android.support.v4.media.MediaBrowserCompat.MediaItem; 31 import android.support.v4.media.MediaBrowserServiceCompat; 32 import android.support.v4.media.session.PlaybackStateCompat; 33 import android.util.Log; 34 35 import com.android.car.broadcastradio.support.Program; 36 import com.android.car.broadcastradio.support.media.BrowseTree; 37 import com.android.car.broadcastradio.support.platform.ProgramSelectorExt; 38 import com.android.car.radio.audio.AudioStreamController; 39 import com.android.car.radio.audio.IPlaybackStateListener; 40 import com.android.car.radio.media.TunerSession; 41 import com.android.car.radio.platform.ImageMemoryCache; 42 import com.android.car.radio.platform.RadioManagerExt; 43 import com.android.car.radio.service.IRadioCallback; 44 import com.android.car.radio.service.IRadioManager; 45 import com.android.car.radio.storage.RadioStorage; 46 47 import java.util.ArrayList; 48 import java.util.HashSet; 49 import java.util.List; 50 import java.util.Objects; 51 import java.util.Optional; 52 53 /** 54 * A persistent {@link Service} that is responsible for opening and closing a {@link RadioTuner}. 55 * All radio operations should be delegated to this class. To be notified of any changes in radio 56 * metadata, register as a {@link android.hardware.radio.RadioTuner.Callback} on this Service. 57 * 58 * <p>Utilize the {@link RadioBinder} to perform radio operations. 59 */ 60 public class RadioService extends MediaBrowserServiceCompat implements IPlaybackStateListener { 61 62 private static String TAG = "BcRadioApp.uisrv"; 63 64 public static String ACTION_UI_SERVICE = "com.android.car.radio.ACTION_UI_SERVICE"; 65 66 /** 67 * The amount of time to wait before re-trying to open the {@link #mRadioTuner}. 68 */ 69 private static final int RADIO_TUNER_REOPEN_DELAY_MS = 5000; 70 71 private final Object mLock = new Object(); 72 73 private int mReOpenRadioTunerCount = 0; 74 private final Handler mHandler = new Handler(); 75 76 private RadioStorage mRadioStorage; 77 private final RadioStorage.PresetsChangeListener mPresetsListener = this::onPresetsChanged; 78 79 private RadioTuner mRadioTuner; 80 81 private boolean mRadioSuccessfullyInitialized; 82 83 private ProgramInfo mCurrentProgram; 84 85 private RadioManagerExt mRadioManager; 86 private ImageMemoryCache mImageCache; 87 88 private AudioStreamController mAudioStreamController; 89 90 private BrowseTree mBrowseTree; 91 private TunerSession mMediaSession; 92 private ProgramList mProgramList; 93 94 /** 95 * Whether or not this {@link RadioService} currently has audio focus, meaning it is the 96 * primary driver of media. Usually, interaction with the radio will be prefaced with an 97 * explicit request for audio focus. However, this is not ideal when muting the radio, so this 98 * state needs to be tracked. 99 */ 100 private boolean mHasAudioFocus; 101 102 /** 103 * An internal {@link android.hardware.radio.RadioTuner.Callback} that will listen for 104 * changes in radio metadata and pass these method calls through to 105 * {@link #mRadioTunerCallbacks}. 106 */ 107 private RadioTuner.Callback mInternalRadioTunerCallback = new InternalRadioCallback(); 108 private List<IRadioCallback> mRadioTunerCallbacks = new ArrayList<>(); 109 110 @Override 111 public IBinder onBind(Intent intent) { 112 if (ACTION_UI_SERVICE.equals(intent.getAction())) { 113 return mBinder; 114 } 115 return super.onBind(intent); 116 } 117 118 @Override 119 public void onCreate() { 120 super.onCreate(); 121 122 if (Log.isLoggable(TAG, Log.DEBUG)) { 123 Log.d(TAG, "onCreate()"); 124 } 125 126 mRadioManager = new RadioManagerExt(this); 127 mAudioStreamController = new AudioStreamController(this, mRadioManager); 128 mRadioStorage = RadioStorage.getInstance(this); 129 mImageCache = new ImageMemoryCache(mRadioManager, 1000); 130 131 mBrowseTree = new BrowseTree(this, mImageCache); 132 mMediaSession = new TunerSession(this, mBrowseTree, mBinder, mImageCache); 133 setSessionToken(mMediaSession.getSessionToken()); 134 mAudioStreamController.addPlaybackStateListener(mMediaSession); 135 mBrowseTree.setAmFmRegionConfig(mRadioManager.getAmFmRegionConfig()); 136 137 mRadioStorage.addPresetsChangeListener(mPresetsListener); 138 onPresetsChanged(); 139 140 mAudioStreamController.addPlaybackStateListener(this); 141 142 openRadioBandInternal(mRadioStorage.getStoredRadioBand()); 143 144 mRadioSuccessfullyInitialized = true; 145 } 146 147 @Override 148 public void onDestroy() { 149 if (Log.isLoggable(TAG, Log.DEBUG)) { 150 Log.d(TAG, "onDestroy()"); 151 } 152 153 mRadioStorage.removePresetsChangeListener(mPresetsListener); 154 mMediaSession.release(); 155 mRadioManager.getRadioTunerExt().close(); 156 close(); 157 158 super.onDestroy(); 159 } 160 161 private void onPresetsChanged() { 162 synchronized (mLock) { 163 mBrowseTree.setFavorites(new HashSet<>(mRadioStorage.getPresets())); 164 mMediaSession.notifyFavoritesChanged(); 165 } 166 } 167 168 /** 169 * Opens the current radio band. Currently, this only supports FM and AM bands. 170 * 171 * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM}, 172 * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}. 173 * @return {@link RadioManager#STATUS_OK} if successful; otherwise, 174 * {@link RadioManager#STATUS_ERROR}. 175 */ 176 private int openRadioBandInternal(int radioBand) { 177 if (!mAudioStreamController.requestMuted(false)) return RadioManager.STATUS_ERROR; 178 179 if (mRadioTuner == null) { 180 mRadioTuner = mRadioManager.openSession(mInternalRadioTunerCallback, null); 181 mProgramList = mRadioTuner.getDynamicProgramList(null); 182 mBrowseTree.setProgramList(mProgramList); 183 } 184 185 if (Log.isLoggable(TAG, Log.DEBUG)) { 186 Log.d(TAG, "openRadioBandInternal() STATUS_OK"); 187 } 188 189 // Reset the counter for exponential backoff each time the radio tuner has been successfully 190 // opened. 191 mReOpenRadioTunerCount = 0; 192 193 tuneToDefault(radioBand); 194 195 return RadioManager.STATUS_OK; 196 } 197 198 private void tuneToDefault(int band) { 199 if (!mAudioStreamController.preparePlayback(Optional.empty())) return; 200 201 long storedChannel = mRadioStorage.getStoredRadioChannel(band); 202 if (storedChannel != RadioStorage.INVALID_RADIO_CHANNEL) { 203 Log.i(TAG, "Restoring stored program: " + storedChannel); 204 mRadioTuner.tune(ProgramSelectorExt.createAmFmSelector(storedChannel)); 205 } else { 206 Log.i(TAG, "No stored program, seeking forward to not play static"); 207 208 // TODO(b/80500464): don't hardcode, pull from tuner config 209 long lastChannel; 210 if (band == RadioManager.BAND_AM) lastChannel = 1620; 211 else lastChannel = 108000; 212 mRadioTuner.tune(ProgramSelectorExt.createAmFmSelector(lastChannel)); 213 214 mRadioTuner.scan(RadioTuner.DIRECTION_UP, true); 215 } 216 } 217 218 /* TODO(b/73950974): remove onRadioMuteChanged from IRadioCallback, 219 * use IPlaybackStateListener directly. 220 */ 221 @Override 222 public void onPlaybackStateChanged(@PlaybackStateCompat.State int state) { 223 boolean muted = state != PlaybackStateCompat.STATE_PLAYING; 224 synchronized (mLock) { 225 for (IRadioCallback callback : mRadioTunerCallbacks) { 226 try { 227 callback.onRadioMuteChanged(muted); 228 } catch (RemoteException e) { 229 Log.e(TAG, "Mute state change callback failed", e); 230 } 231 } 232 } 233 } 234 235 /** 236 * Closes any active {@link RadioTuner}s and releases audio focus. 237 */ 238 private void close() { 239 if (Log.isLoggable(TAG, Log.DEBUG)) { 240 Log.d(TAG, "close()"); 241 } 242 243 mAudioStreamController.requestMuted(true); 244 245 if (mProgramList != null) { 246 mProgramList.close(); 247 mProgramList = null; 248 } 249 if (mRadioTuner != null) { 250 mRadioTuner.close(); 251 mRadioTuner = null; 252 } 253 } 254 255 private IRadioManager.Stub mBinder = new IRadioManager.Stub() { 256 /** 257 * Tunes the radio to the given frequency. To be notified of a successful tune, register 258 * as a {@link android.hardware.radio.RadioTuner.Callback}. 259 */ 260 @Override 261 public void tune(ProgramSelector sel) { 262 if (!mAudioStreamController.preparePlayback(Optional.empty())) return; 263 mRadioTuner.tune(sel); 264 } 265 266 @Override 267 public List<ProgramInfo> getProgramList() { 268 return mRadioTuner.getDynamicProgramList(null).toList(); 269 } 270 271 /** 272 * Seeks the radio forward. To be notified of a successful tune, register as a 273 * {@link android.hardware.radio.RadioTuner.Callback}. 274 */ 275 @Override 276 public void seekForward() { 277 if (!mAudioStreamController.preparePlayback(Optional.of(true))) return; 278 279 if (mRadioTuner == null) { 280 int radioStatus = openRadioBandInternal(mRadioStorage.getStoredRadioBand()); 281 if (radioStatus == RadioManager.STATUS_ERROR) { 282 return; 283 } 284 } 285 286 mRadioTuner.scan(RadioTuner.DIRECTION_UP, true); 287 } 288 289 /** 290 * Seeks the radio backwards. To be notified of a successful tune, register as a 291 * {@link android.hardware.radio.RadioTuner.Callback}. 292 */ 293 @Override 294 public void seekBackward() { 295 if (!mAudioStreamController.preparePlayback(Optional.of(false))) return; 296 297 if (mRadioTuner == null) { 298 int radioStatus = openRadioBandInternal(mRadioStorage.getStoredRadioBand()); 299 if (radioStatus == RadioManager.STATUS_ERROR) { 300 return; 301 } 302 } 303 304 mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, true); 305 } 306 307 /** 308 * Mutes the radio. 309 * 310 * @return {@code true} if the mute was successful. 311 */ 312 @Override 313 public boolean mute() { 314 return mAudioStreamController.requestMuted(true); 315 } 316 317 /** 318 * Un-mutes the radio and causes audio to play. 319 * 320 * @return {@code true} if the un-mute was successful. 321 */ 322 @Override 323 public boolean unMute() { 324 return mAudioStreamController.requestMuted(false); 325 } 326 327 /** 328 * Returns {@code true} if the radio is currently muted. 329 */ 330 @Override 331 public boolean isMuted() { 332 return mAudioStreamController.isMuted(); 333 } 334 335 @Override 336 public void addFavorite(Program program) { 337 mRadioStorage.storePreset(program); 338 } 339 340 @Override 341 public void removeFavorite(ProgramSelector sel) { 342 mRadioStorage.removePreset(sel); 343 } 344 345 @Override 346 public void switchBand(int radioBand) { 347 tuneToDefault(radioBand); 348 } 349 350 /** 351 * Adds the given {@link android.hardware.radio.RadioTuner.Callback} to be notified 352 * of any radio metadata changes. 353 */ 354 @Override 355 public void addRadioTunerCallback(IRadioCallback callback) { 356 if (callback == null) { 357 return; 358 } 359 360 mRadioTunerCallbacks.add(callback); 361 } 362 363 /** 364 * Removes the given {@link android.hardware.radio.RadioTuner.Callback} from receiving 365 * any radio metadata chagnes. 366 */ 367 @Override 368 public void removeRadioTunerCallback(IRadioCallback callback) { 369 if (callback == null) { 370 return; 371 } 372 373 mRadioTunerCallbacks.remove(callback); 374 } 375 376 @Override 377 public ProgramInfo getCurrentProgramInfo() { 378 return mCurrentProgram; 379 } 380 381 /** 382 * Returns {@code true} if the radio was able to successfully initialize. A value of 383 * {@code false} here could mean that the {@code RadioService} was not able to connect to 384 * the {@link RadioManager} or there were no radio modules on the current device. 385 */ 386 @Override 387 public boolean isInitialized() { 388 return mRadioSuccessfullyInitialized; 389 } 390 391 /** 392 * Returns {@code true} if the radio currently has focus and is therefore the application 393 * that is supplying music. 394 */ 395 @Override 396 public boolean hasFocus() { 397 return mHasAudioFocus; 398 } 399 }; 400 401 /** 402 * A extension of {@link android.hardware.radio.RadioTuner.Callback} that delegates to a 403 * callback registered on this service. 404 */ 405 private class InternalRadioCallback extends RadioTuner.Callback { 406 @Override 407 public void onProgramInfoChanged(ProgramInfo info) { 408 if (Log.isLoggable(TAG, Log.DEBUG)) { 409 Log.d(TAG, "Program info changed: " + info); 410 } 411 412 mCurrentProgram = Objects.requireNonNull(info); 413 mMediaSession.notifyProgramInfoChanged(info); 414 mAudioStreamController.notifyProgramInfoChanged(); 415 mRadioStorage.storeRadioChannel(info.getSelector()); 416 417 for (IRadioCallback callback : mRadioTunerCallbacks) { 418 try { 419 callback.onCurrentProgramInfoChanged(info); 420 } catch (RemoteException e) { 421 Log.e(TAG, "Failed to notify about changed radio station", e); 422 } 423 } 424 } 425 426 @Override 427 public void onError(int status) { 428 Log.e(TAG, "onError(); status: " + status); 429 430 // If there is a hardware failure or the radio service died, then this requires a 431 // re-opening of the radio tuner. 432 if (status == RadioTuner.ERROR_HARDWARE_FAILURE 433 || status == RadioTuner.ERROR_SERVER_DIED) { 434 close(); 435 436 // Attempt to re-open the RadioTuner. Each time the radio tuner fails to open, the 437 // mReOpenRadioTunerCount will be incremented. 438 mHandler.removeCallbacks(mOpenRadioTunerRunnable); 439 mHandler.postDelayed(mOpenRadioTunerRunnable, 440 mReOpenRadioTunerCount * RADIO_TUNER_REOPEN_DELAY_MS); 441 442 mReOpenRadioTunerCount++; 443 } 444 445 try { 446 for (IRadioCallback callback : mRadioTunerCallbacks) { 447 callback.onError(status); 448 } 449 } catch (RemoteException e) { 450 Log.e(TAG, "onError(); Failed to notify IRadioCallbacks: " + e.getMessage()); 451 } 452 } 453 454 @Override 455 public void onControlChanged(boolean control) { 456 // If the radio loses control of the RadioTuner, then close it and allow it to be 457 // re-opened when control has been gained. 458 if (!control) { 459 close(); 460 return; 461 } 462 463 if (mRadioTuner == null) { 464 openRadioBandInternal(mRadioStorage.getStoredRadioBand()); 465 } 466 } 467 } 468 469 private final Runnable mOpenRadioTunerRunnable = 470 () -> openRadioBandInternal(mRadioStorage.getStoredRadioBand()); 471 472 @Override 473 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { 474 /* Radio application may restrict who can read its MediaBrowser tree. 475 * Our implementation doesn't. 476 */ 477 return mBrowseTree.getRoot(); 478 } 479 480 @Override 481 public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) { 482 mBrowseTree.loadChildren(parentMediaId, result); 483 } 484 485 @Override 486 public int onStartCommand(Intent intent, int flags, int startId) { 487 if (BrowseTree.ACTION_PLAY_BROADCASTRADIO.equals(intent.getAction())) { 488 Log.i(TAG, "Executing general play radio intent"); 489 mMediaSession.getController().getTransportControls().playFromMediaId( 490 mBrowseTree.getRoot().getRootId(), null); 491 return START_NOT_STICKY; 492 } 493 494 return super.onStartCommand(intent, flags, startId); 495 } 496 497 @Override 498 public IBinder asBinder() { 499 throw new UnsupportedOperationException("Not a binder"); 500 } 501 } 502