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.car.hardware.radio.CarRadioManager; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.PackageManager; 24 import android.hardware.radio.RadioManager; 25 import android.hardware.radio.RadioMetadata; 26 import android.hardware.radio.RadioTuner; 27 import android.media.AudioAttributes; 28 import android.media.AudioManager; 29 import android.os.Handler; 30 import android.os.IBinder; 31 import android.os.RemoteException; 32 import android.os.SystemProperties; 33 import android.support.annotation.Nullable; 34 import android.support.car.Car; 35 import android.support.car.CarNotConnectedException; 36 import android.support.car.CarConnectionCallback; 37 import android.support.car.media.CarAudioManager; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import com.android.car.radio.demo.RadioDemo; 41 import com.android.car.radio.service.IRadioCallback; 42 import com.android.car.radio.service.IRadioManager; 43 import com.android.car.radio.service.RadioRds; 44 import com.android.car.radio.service.RadioStation; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 49 /** 50 * A persistent {@link Service} that is responsible for opening and closing a {@link RadioTuner}. 51 * All radio operations should be delegated to this class. To be notified of any changes in radio 52 * metadata, register as a {@link android.hardware.radio.RadioTuner.Callback} on this Service. 53 * 54 * <p>Utilize the {@link RadioBinder} to perform radio operations. 55 */ 56 public class RadioService extends Service implements AudioManager.OnAudioFocusChangeListener { 57 private static String TAG = "Em.RadioService"; 58 59 /** 60 * The amount of time to wait before re-trying to open the {@link #mRadioTuner}. 61 */ 62 private static final int RADIO_TUNER_REOPEN_DELAY_MS = 5000; 63 64 private int mReOpenRadioTunerCount = 0; 65 private final Handler mHandler = new Handler(); 66 67 private Car mCarApi; 68 private RadioTuner mRadioTuner; 69 70 private boolean mRadioSuccessfullyInitialized; 71 private int mCurrentRadioBand = RadioManager.BAND_FM; 72 private int mCurrentRadioChannel = RadioStorage.INVALID_RADIO_CHANNEL; 73 74 private String mCurrentChannelInfo; 75 private String mCurrentArtist; 76 private String mCurrentSongTitle; 77 78 private RadioManager mRadioManager; 79 private RadioBackgroundScanner mBackgroundScanner; 80 private RadioManager.FmBandDescriptor mFmDescriptor; 81 private RadioManager.AmBandDescriptor mAmDescriptor; 82 83 private RadioManager.FmBandConfig mFmConfig; 84 private RadioManager.AmBandConfig mAmConfig; 85 86 private final List<RadioManager.ModuleProperties> mModules = new ArrayList<>(); 87 88 private CarAudioManager mCarAudioManager; 89 private AudioAttributes mRadioAudioAttributes; 90 91 /** 92 * Whether or not this {@link RadioService} currently has audio focus, meaning it is the 93 * primary driver of media. Usually, interaction with the radio will be prefaced with an 94 * explicit request for audio focus. However, this is not ideal when muting the radio, so this 95 * state needs to be tracked. 96 */ 97 private boolean mHasAudioFocus; 98 99 /** 100 * An internal {@link android.hardware.radio.RadioTuner.Callback} that will listen for 101 * changes in radio metadata and pass these method calls through to 102 * {@link #mRadioTunerCallbacks}. 103 */ 104 private RadioTuner.Callback mInternalRadioTunerCallback = new InternalRadioCallback(); 105 private List<IRadioCallback> mRadioTunerCallbacks = new ArrayList<>(); 106 107 @Override onBind(Intent intent)108 public IBinder onBind(Intent intent) { 109 if (Log.isLoggable(TAG, Log.DEBUG)) { 110 Log.d(TAG, "onBind(); Intent: " + intent); 111 } 112 return mBinder; 113 } 114 115 @Override onCreate()116 public void onCreate() { 117 super.onCreate(); 118 119 if (Log.isLoggable(TAG, Log.DEBUG)) { 120 Log.d(TAG, "onCreate()"); 121 } 122 123 // Connection to car services does not work for non-automotive yet, so this call needs to 124 // be guarded. 125 if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { 126 mCarApi = Car.createCar(this /* context */, mCarConnectionCallback); 127 mCarApi.connect(); 128 } 129 130 if (SystemProperties.getBoolean(RadioDemo.DEMO_MODE_PROPERTY, false)) { 131 initializeDemo(); 132 } else { 133 initialze(); 134 } 135 } 136 137 /** 138 * Initializes this service to use a demo {@link IRadioManager}. 139 * 140 * @see {@link RadioDemo} 141 */ initializeDemo()142 private void initializeDemo() { 143 if (Log.isLoggable(TAG, Log.DEBUG)) { 144 Log.d(TAG, "initializeDemo()"); 145 } 146 147 mBinder = RadioDemo.getInstance(this /* context */).createDemoManager(); 148 } 149 150 /** 151 * Connects to the {@link RadioManager}. 152 */ initialze()153 private void initialze() { 154 mRadioManager = (RadioManager) getSystemService(Context.RADIO_SERVICE); 155 156 if (Log.isLoggable(TAG, Log.DEBUG)) { 157 Log.d(TAG, "initialze(); mRadioManager: " + mRadioManager); 158 } 159 160 if (mRadioManager == null) { 161 Log.w(TAG, "RadioManager could not be loaded."); 162 return; 163 } 164 165 int status = mRadioManager.listModules(mModules); 166 if (status != RadioManager.STATUS_OK) { 167 Log.w(TAG, "Load modules failed with status: " + status); 168 return; 169 } 170 171 if (Log.isLoggable(TAG, Log.DEBUG)) { 172 Log.d(TAG, "initialze(); listModules complete: " + mModules); 173 } 174 175 if (mModules.size() == 0) { 176 Log.w(TAG, "No radio modules on device."); 177 return; 178 } 179 180 boolean isDebugLoggable = Log.isLoggable(TAG, Log.DEBUG); 181 182 // Load the possible radio bands. For now, just accept FM and AM bands. 183 for (RadioManager.BandDescriptor band : mModules.get(0).getBands()) { 184 if (isDebugLoggable) { 185 Log.d(TAG, "loading band: " + band.toString()); 186 } 187 188 if (mFmDescriptor == null && band.getType() == RadioManager.BAND_FM) { 189 mFmDescriptor = (RadioManager.FmBandDescriptor) band; 190 } 191 192 if (mAmDescriptor == null && band.getType() == RadioManager.BAND_AM) { 193 mAmDescriptor = (RadioManager.AmBandDescriptor) band; 194 } 195 } 196 197 if (mFmDescriptor == null && mAmDescriptor == null) { 198 Log.w(TAG, "No AM and FM radio bands could be loaded."); 199 return; 200 } 201 202 // TODO: Make stereo configurable depending on device. 203 mFmConfig = new RadioManager.FmBandConfig.Builder(mFmDescriptor) 204 .setStereo(true) 205 .build(); 206 mAmConfig = new RadioManager.AmBandConfig.Builder(mAmDescriptor) 207 .setStereo(true) 208 .build(); 209 210 // If there is a second tuner on the device, then set it up as the background scanner. 211 if (mModules.size() >= 2) { 212 if (isDebugLoggable) { 213 Log.d(TAG, "Second tuner detected on device; setting up background scanner"); 214 } 215 216 mBackgroundScanner = new RadioBackgroundScanner(this /* context */, mRadioManager, 217 mAmConfig, mFmConfig, mModules.get(1)); 218 } 219 220 mRadioSuccessfullyInitialized = true; 221 } 222 223 @Override onDestroy()224 public void onDestroy() { 225 if (Log.isLoggable(TAG, Log.DEBUG)) { 226 Log.d(TAG, "onDestroy()"); 227 } 228 229 close(); 230 231 if (mCarApi != null) { 232 mCarApi.disconnect(); 233 } 234 235 super.onDestroy(); 236 } 237 238 /** 239 * Opens the current radio band. Currently, this only supports FM and AM bands. 240 * 241 * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM}, 242 * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}. 243 * @return {@link RadioManager#STATUS_OK} if successful; otherwise, 244 * {@link RadioManager#STATUS_ERROR}. 245 */ openRadioBandInternal(int radioBand)246 private int openRadioBandInternal(int radioBand) { 247 if (requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 248 Log.e(TAG, "openRadioBandInternal() audio focus request fail"); 249 return RadioManager.STATUS_ERROR; 250 } 251 252 mCurrentRadioBand = radioBand; 253 RadioManager.BandConfig config = getRadioConfig(radioBand); 254 255 if (config == null) { 256 Log.w(TAG, "Cannot create config for radio band: " + radioBand); 257 return RadioManager.STATUS_ERROR; 258 } 259 260 if (mRadioTuner != null) { 261 mRadioTuner.setConfiguration(config); 262 } else { 263 mRadioTuner = mRadioManager.openTuner(mModules.get(0).getId(), config, true, 264 mInternalRadioTunerCallback, null /* handler */); 265 } 266 267 if (Log.isLoggable(TAG, Log.DEBUG)) { 268 Log.d(TAG, "openRadioBandInternal() STATUS_OK"); 269 } 270 271 if (mBackgroundScanner != null) { 272 mBackgroundScanner.onRadioBandChanged(radioBand); 273 } 274 275 // Reset the counter for exponential backoff each time the radio tuner has been successfully 276 // opened. 277 mReOpenRadioTunerCount = 0; 278 279 return RadioManager.STATUS_OK; 280 } 281 282 /** 283 * Returns a {@link RadioRds} object that holds all the current radio metadata. If all the 284 * metadata is empty, then {@code null} is returned. 285 */ 286 @Nullable createCurrentRadioRds()287 private RadioRds createCurrentRadioRds() { 288 if (TextUtils.isEmpty(mCurrentChannelInfo) && TextUtils.isEmpty(mCurrentArtist) 289 && TextUtils.isEmpty(mCurrentSongTitle)) { 290 return null; 291 } 292 293 return new RadioRds(mCurrentChannelInfo, mCurrentArtist, mCurrentSongTitle); 294 } 295 296 /** 297 * Creates a {@link RadioStation} that encapsulates all the information about the current 298 * radio station. 299 */ createCurrentRadioStation()300 private RadioStation createCurrentRadioStation() { 301 // mCurrentRadioChannel can possibly be invalid if this class never receives a callback 302 // for onProgramInfoChanged(). As a result, manually retrieve the information for the 303 // current station from RadioTuner if this is the case. 304 if (mCurrentRadioChannel == RadioStorage.INVALID_RADIO_CHANNEL && mRadioTuner != null) { 305 if (Log.isLoggable(TAG, Log.DEBUG)) { 306 Log.d(TAG, "createCurrentRadioStation(); invalid current radio channel. " 307 + "Calling getProgramInformation for valid station"); 308 } 309 310 // getProgramInformation() expects an array of size 1. 311 RadioManager.ProgramInfo[] info = new RadioManager.ProgramInfo[1]; 312 int status = mRadioTuner.getProgramInformation(info); 313 314 if (Log.isLoggable(TAG, Log.DEBUG)) { 315 Log.d(TAG, "getProgramInformation() status: " + status + "; info: " + info[0]); 316 } 317 318 if (status == RadioManager.STATUS_OK && info[0] != null) { 319 mCurrentRadioChannel = info[0].getChannel(); 320 321 if (Log.isLoggable(TAG, Log.DEBUG)) { 322 Log.d(TAG, "program info channel: " + mCurrentRadioChannel); 323 } 324 } 325 } 326 327 return new RadioStation(mCurrentRadioChannel, 0 /* subChannelNumber */, 328 mCurrentRadioBand, createCurrentRadioRds()); 329 } 330 331 /** 332 * Returns the proper {@link android.hardware.radio.RadioManager.BandConfig} for the given 333 * radio band. {@code null} is returned if the band is not suppored. 334 */ 335 @Nullable getRadioConfig(int selectedRadioBand)336 private RadioManager.BandConfig getRadioConfig(int selectedRadioBand) { 337 switch (selectedRadioBand) { 338 case RadioManager.BAND_AM: 339 return mAmConfig; 340 case RadioManager.BAND_FM: 341 return mFmConfig; 342 343 // TODO: Support BAND_FM_HD and BAND_AM_HD. 344 345 default: 346 return null; 347 } 348 } 349 requestAudioFocus()350 private int requestAudioFocus() { 351 int status = AudioManager.AUDIOFOCUS_REQUEST_FAILED; 352 try { 353 status = mCarAudioManager.requestAudioFocus(this, mRadioAudioAttributes, 354 AudioManager.AUDIOFOCUS_GAIN, 0); 355 } catch (CarNotConnectedException e) { 356 Log.e(TAG, "requestAudioFocus() failed", e); 357 } 358 359 if (Log.isLoggable(TAG, Log.DEBUG)) { 360 Log.d(TAG, "requestAudioFocus status: " + status); 361 } 362 363 if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 364 mHasAudioFocus = true; 365 366 // Receiving audio focus means that the radio is un-muted. 367 for (IRadioCallback callback : mRadioTunerCallbacks) { 368 try { 369 callback.onRadioMuteChanged(false); 370 } catch (RemoteException e) { 371 Log.e(TAG, "requestAudioFocus(); onRadioMuteChanged() notify failed: " 372 + e.getMessage()); 373 } 374 } 375 } 376 377 return status; 378 } 379 abandonAudioFocus()380 private void abandonAudioFocus() { 381 if (Log.isLoggable(TAG, Log.DEBUG)) { 382 Log.d(TAG, "abandonAudioFocus()"); 383 } 384 385 if (mCarAudioManager == null) { 386 return; 387 } 388 389 mCarAudioManager.abandonAudioFocus(this, mRadioAudioAttributes); 390 mHasAudioFocus = false; 391 392 for (IRadioCallback callback : mRadioTunerCallbacks) { 393 try { 394 callback.onRadioMuteChanged(true); 395 } catch (RemoteException e) { 396 Log.e(TAG, "abandonAudioFocus(); onRadioMutechanged() notify failed: " 397 + e.getMessage()); 398 } 399 } 400 } 401 402 /** 403 * Closes any active {@link RadioTuner}s and releases audio focus. 404 */ close()405 private void close() { 406 if (Log.isLoggable(TAG, Log.DEBUG)) { 407 Log.d(TAG, "close()"); 408 } 409 410 abandonAudioFocus(); 411 412 if (mRadioTuner != null) { 413 mRadioTuner.close(); 414 mRadioTuner = null; 415 } 416 } 417 418 @Override onAudioFocusChange(int focusChange)419 public void onAudioFocusChange(int focusChange) { 420 if (Log.isLoggable(TAG, Log.DEBUG)) { 421 Log.d(TAG, "focus change: " + focusChange); 422 } 423 424 switch (focusChange) { 425 case AudioManager.AUDIOFOCUS_GAIN: 426 mHasAudioFocus = true; 427 openRadioBandInternal(mCurrentRadioBand); 428 break; 429 430 // For a transient loss, just allow the focus to be released. The radio will stop 431 // itself automatically. There is no need for an explicit abandon audio focus call 432 // because this removes the AudioFocusChangeListener. 433 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 434 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 435 mHasAudioFocus = false; 436 break; 437 438 case AudioManager.AUDIOFOCUS_LOSS: 439 close(); 440 break; 441 442 default: 443 // Do nothing for all other cases. 444 } 445 } 446 447 /** 448 * {@link CarConnectionCallback} that retrieves the {@link CarRadioManager}. 449 */ 450 private final CarConnectionCallback mCarConnectionCallback = 451 new CarConnectionCallback() { 452 @Override 453 public void onConnected(Car car) { 454 if (Log.isLoggable(TAG, Log.DEBUG)) { 455 Log.d(TAG, "Car service connected."); 456 } 457 try { 458 // The CarAudioManager only needs to be retrieved once. 459 if (mCarAudioManager == null) { 460 mCarAudioManager = (CarAudioManager) mCarApi.getCarManager( 461 android.car.Car.AUDIO_SERVICE); 462 463 mRadioAudioAttributes = mCarAudioManager.getAudioAttributesForCarUsage( 464 CarAudioManager.CAR_AUDIO_USAGE_RADIO); 465 } 466 } catch (CarNotConnectedException e) { 467 //TODO finish 468 Log.e(TAG, "Car not connected"); 469 } 470 } 471 472 @Override 473 public void onDisconnected(Car car) { 474 if (Log.isLoggable(TAG, Log.DEBUG)) { 475 Log.d(TAG, "Car service disconnected."); 476 } 477 } 478 }; 479 480 private IRadioManager.Stub mBinder = new IRadioManager.Stub() { 481 /** 482 * Tunes the radio to the given frequency. To be notified of a successful tune, register 483 * as a {@link android.hardware.radio.RadioTuner.Callback}. 484 */ 485 @Override 486 public void tune(RadioStation radioStation) { 487 if (mRadioManager == null || radioStation == null 488 || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 489 return; 490 } 491 492 if (mRadioTuner == null || radioStation.getRadioBand() != mCurrentRadioBand) { 493 int radioStatus = openRadioBandInternal(radioStation.getRadioBand()); 494 if (radioStatus == RadioManager.STATUS_ERROR) { 495 return; 496 } 497 } 498 499 int status = mRadioTuner.tune(radioStation.getChannelNumber(), 0 /* subChannel */); 500 501 if (Log.isLoggable(TAG, Log.DEBUG)) { 502 Log.d(TAG, "Tuning to station: " + radioStation + "\n\tstatus: " + status); 503 } 504 } 505 506 /** 507 * Seeks the radio forward. To be notified of a successful tune, register as a 508 * {@link android.hardware.radio.RadioTuner.Callback}. 509 */ 510 @Override 511 public void seekForward() { 512 if (mRadioManager == null 513 || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 514 return; 515 } 516 517 if (mRadioTuner == null) { 518 int radioStatus = openRadioBandInternal(mCurrentRadioBand); 519 if (radioStatus == RadioManager.STATUS_ERROR) { 520 return; 521 } 522 } 523 524 mRadioTuner.scan(RadioTuner.DIRECTION_UP, true); 525 } 526 527 /** 528 * Seeks the radio backwards. To be notified of a successful tune, register as a 529 * {@link android.hardware.radio.RadioTuner.Callback}. 530 */ 531 @Override 532 public void seekBackward() { 533 if (mRadioManager == null 534 || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 535 return; 536 } 537 538 if (mRadioTuner == null) { 539 int radioStatus = openRadioBandInternal(mCurrentRadioBand); 540 if (radioStatus == RadioManager.STATUS_ERROR) { 541 return; 542 } 543 } 544 545 mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, true); 546 } 547 548 /** 549 * Mutes the radio. 550 * 551 * @return {@code true} if the mute was successful. 552 */ 553 @Override 554 public boolean mute() { 555 if (mRadioManager == null) { 556 return false; 557 } 558 559 if (mCarAudioManager == null) { 560 if (Log.isLoggable(TAG, Log.DEBUG)) { 561 Log.d(TAG, "mute() called, but not connected to CarAudioManager"); 562 } 563 return false; 564 } 565 566 // If the radio does not currently have focus, then no need to do anything because the 567 // radio won't be playing any sound. 568 if (!mHasAudioFocus) { 569 if (Log.isLoggable(TAG, Log.DEBUG)) { 570 Log.d(TAG, "mute() called, but radio does not currently have audio focus; " 571 + "ignoring."); 572 } 573 return false; 574 } 575 576 boolean muteSuccessful = false; 577 578 try { 579 muteSuccessful = mCarAudioManager.setMediaMute(true); 580 581 if (Log.isLoggable(TAG, Log.DEBUG)) { 582 Log.d(TAG, "setMediaMute(true) status: " + muteSuccessful); 583 } 584 } catch (CarNotConnectedException e) { 585 Log.e(TAG, "mute() failed: " + e.getMessage()); 586 e.printStackTrace(); 587 } 588 589 if (muteSuccessful && mRadioTunerCallbacks.size() > 0) { 590 for (IRadioCallback callback : mRadioTunerCallbacks) { 591 try { 592 callback.onRadioMuteChanged(true); 593 } catch (RemoteException e) { 594 Log.e(TAG, "mute() notify failed: " + e.getMessage()); 595 } 596 } 597 } 598 599 return muteSuccessful; 600 } 601 602 /** 603 * Un-mutes the radio and causes audio to play. 604 * 605 * @return {@code true} if the un-mute was successful. 606 */ 607 @Override 608 public boolean unMute() { 609 if (mRadioManager == null) { 610 return false; 611 } 612 613 if (mCarAudioManager == null) { 614 if (Log.isLoggable(TAG, Log.DEBUG)) { 615 Log.d(TAG, "toggleMute() called, but not connected to CarAudioManager"); 616 } 617 return false; 618 } 619 620 // Requesting audio focus will automatically un-mute the radio if it had been muted. 621 return requestAudioFocus() == AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 622 } 623 624 /** 625 * Returns {@code true} if the radio is currently muted. 626 */ 627 @Override 628 public boolean isMuted() { 629 if (!mHasAudioFocus) { 630 return true; 631 } 632 633 if (mRadioManager == null) { 634 return true; 635 } 636 637 if (mCarAudioManager == null) { 638 if (Log.isLoggable(TAG, Log.DEBUG)) { 639 Log.d(TAG, "isMuted() called, but not connected to CarAudioManager"); 640 } 641 return true; 642 } 643 644 boolean isMuted = false; 645 646 try { 647 isMuted = mCarAudioManager.isMediaMuted(); 648 } catch (CarNotConnectedException e) { 649 Log.e(TAG, "isMuted() failed: " + e.getMessage()); 650 e.printStackTrace(); 651 } 652 653 return isMuted; 654 } 655 656 /** 657 * Opens the radio for the given band. 658 * 659 * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM}, 660 * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}. 661 * @return {@link RadioManager#STATUS_OK} if successful; otherwise, 662 * {@link RadioManager#STATUS_ERROR}. 663 */ 664 @Override 665 public int openRadioBand(int radioBand) { 666 if (Log.isLoggable(TAG, Log.DEBUG)) { 667 Log.d(TAG, "openRadioBand() for band: " + radioBand); 668 } 669 670 if (mRadioManager == null) { 671 return RadioManager.STATUS_ERROR; 672 } 673 674 return openRadioBandInternal(radioBand); 675 } 676 677 /** 678 * Adds the given {@link android.hardware.radio.RadioTuner.Callback} to be notified 679 * of any radio metadata changes. 680 */ 681 @Override 682 public void addRadioTunerCallback(IRadioCallback callback) { 683 if (callback == null) { 684 return; 685 } 686 687 mRadioTunerCallbacks.add(callback); 688 } 689 690 /** 691 * Removes the given {@link android.hardware.radio.RadioTuner.Callback} from receiving 692 * any radio metadata chagnes. 693 */ 694 @Override 695 public void removeRadioTunerCallback(IRadioCallback callback) { 696 if (callback == null) { 697 return; 698 } 699 700 mRadioTunerCallbacks.remove(callback); 701 } 702 703 /** 704 * Returns a {@link RadioStation} that encapsulates the information about the current 705 * station the radio is tuned to. 706 */ 707 @Override 708 public RadioStation getCurrentRadioStation() { 709 return createCurrentRadioStation(); 710 } 711 712 /** 713 * Returns {@code true} if the radio was able to successfully initialize. A value of 714 * {@code false} here could mean that the {@code RadioService} was not able to connect to 715 * the {@link RadioManager} or there were no radio modules on the current device. 716 */ 717 @Override 718 public boolean isInitialized() { 719 return mRadioSuccessfullyInitialized; 720 } 721 722 /** 723 * Returns {@code true} if the radio currently has focus and is therefore the application 724 * that is supplying music. 725 */ 726 @Override 727 public boolean hasFocus() { 728 return mHasAudioFocus; 729 } 730 731 /** 732 * Returns {@code true} if the current radio module has dual tuners, meaning that a tuner 733 * is available to scan for stations in the background. 734 */ 735 @Override 736 public boolean hasDualTuners() { 737 return mModules.size() >= 2; 738 } 739 }; 740 741 /** 742 * A extension of {@link android.hardware.radio.RadioTuner.Callback} that delegates to a 743 * callback registered on this service. 744 */ 745 private class InternalRadioCallback extends RadioTuner.Callback { 746 @Override onProgramInfoChanged(RadioManager.ProgramInfo info)747 public void onProgramInfoChanged(RadioManager.ProgramInfo info) { 748 if (Log.isLoggable(TAG, Log.DEBUG)) { 749 Log.d(TAG, "onProgramInfoChanged(); info: " + info); 750 } 751 752 clearMetadata(); 753 754 if (info != null) { 755 mCurrentRadioChannel = info.getChannel(); 756 757 if (Log.isLoggable(TAG, Log.DEBUG)) { 758 Log.d(TAG, "onProgramInfoChanged(); info channel: " + mCurrentRadioChannel); 759 } 760 } 761 762 RadioStation station = createCurrentRadioStation(); 763 764 try { 765 for (IRadioCallback callback : mRadioTunerCallbacks) { 766 callback.onRadioStationChanged(station); 767 } 768 } catch (RemoteException e) { 769 Log.e(TAG, "onProgramInfoChanged(); " 770 + "Failed to notify IRadioCallbacks: " + e.getMessage()); 771 } 772 } 773 774 @Override onMetadataChanged(RadioMetadata metadata)775 public void onMetadataChanged(RadioMetadata metadata) { 776 if (Log.isLoggable(TAG, Log.DEBUG)) { 777 Log.d(TAG, "onMetadataChanged(); metadata: " + metadata); 778 } 779 780 clearMetadata(); 781 updateMetadata(metadata); 782 783 RadioRds radioRds = createCurrentRadioRds(); 784 785 try { 786 for (IRadioCallback callback : mRadioTunerCallbacks) { 787 callback.onRadioMetadataChanged(radioRds); 788 } 789 } catch (RemoteException e) { 790 Log.e(TAG, "onMetadataChanged(); " 791 + "Failed to notify IRadioCallbacks: " + e.getMessage()); 792 } 793 } 794 795 @Override onConfigurationChanged(RadioManager.BandConfig config)796 public void onConfigurationChanged(RadioManager.BandConfig config) { 797 if (Log.isLoggable(TAG, Log.DEBUG)) { 798 Log.d(TAG, "onConfigurationChanged(): config: " + config); 799 } 800 801 clearMetadata(); 802 803 if (config != null) { 804 mCurrentRadioBand = config.getType(); 805 806 if (Log.isLoggable(TAG, Log.DEBUG)) { 807 Log.d(TAG, "onConfigurationChanged(): config type: " + mCurrentRadioBand); 808 } 809 810 } 811 812 try { 813 for (IRadioCallback callback : mRadioTunerCallbacks) { 814 callback.onRadioBandChanged(mCurrentRadioBand); 815 } 816 } catch (RemoteException e) { 817 Log.e(TAG, "onConfigurationChanged(); " 818 + "Failed to notify IRadioCallbacks: " + e.getMessage()); 819 } 820 } 821 822 @Override onError(int status)823 public void onError(int status) { 824 Log.e(TAG, "onError(); status: " + status); 825 826 // If there is a hardware failure or the radio service died, then this requires a 827 // re-opening of the radio tuner. 828 if (status == RadioTuner.ERROR_HARDWARE_FAILURE 829 || status == RadioTuner.ERROR_SERVER_DIED) { 830 if (mRadioTuner != null) { 831 mRadioTuner.close(); 832 mRadioTuner = null; 833 } 834 835 // Attempt to re-open the RadioTuner. Each time the radio tuner fails to open, the 836 // mReOpenRadioTunerCount will be incremented. 837 mHandler.removeCallbacks(mOpenRadioTunerRunnable); 838 mHandler.postDelayed(mOpenRadioTunerRunnable, 839 mReOpenRadioTunerCount * RADIO_TUNER_REOPEN_DELAY_MS); 840 841 mReOpenRadioTunerCount++; 842 } 843 844 try { 845 for (IRadioCallback callback : mRadioTunerCallbacks) { 846 callback.onError(status); 847 } 848 } catch (RemoteException e) { 849 Log.e(TAG, "onError(); Failed to notify IRadioCallbacks: " + e.getMessage()); 850 } 851 } 852 853 @Override onControlChanged(boolean control)854 public void onControlChanged(boolean control) { 855 // If the radio loses control of the RadioTuner, then close it and allow it to be 856 // re-opened when control has been gained. 857 if (!control) { 858 close(); 859 return; 860 } 861 862 if (mRadioTuner == null) { 863 openRadioBandInternal(mCurrentRadioBand); 864 } 865 } 866 867 /** 868 * Sets all metadata fields to {@code null}. 869 */ clearMetadata()870 private void clearMetadata() { 871 mCurrentChannelInfo = null; 872 mCurrentArtist = null; 873 mCurrentSongTitle = null; 874 } 875 876 /** 877 * Retrieves the relevant information off the given {@link RadioMetadata} object and 878 * sets them correspondingly on {@link #mCurrentChannelInfo}, {@link #mCurrentArtist} 879 * and {@link #mCurrentSongTitle}. 880 */ updateMetadata(RadioMetadata metadata)881 private void updateMetadata(RadioMetadata metadata) { 882 if (metadata != null) { 883 mCurrentChannelInfo = metadata.getString(RadioMetadata.METADATA_KEY_RDS_PS); 884 mCurrentArtist = metadata.getString(RadioMetadata.METADATA_KEY_ARTIST); 885 mCurrentSongTitle = metadata.getString(RadioMetadata.METADATA_KEY_TITLE); 886 887 if (Log.isLoggable(TAG, Log.DEBUG)) { 888 Log.d(TAG, String.format("updateMetadata(): [channel info: %s, artist: %s, " 889 + "song title: %s]", mCurrentChannelInfo, mCurrentArtist, 890 mCurrentSongTitle)); 891 } 892 } 893 } 894 } 895 896 private final Runnable mOpenRadioTunerRunnable = () -> openRadioBandInternal(mCurrentRadioBand); 897 } 898