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 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 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 RadioDemo 141 */ 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 */ 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.isFmBand()) { 189 mFmDescriptor = (RadioManager.FmBandDescriptor) band; 190 } 191 192 if (mAmDescriptor == null && band.isAmBand()) { 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 // TODO(b/63101896): we don't know if the second tuner is for the same medium, so we don't 212 // set background scanner for now. 213 214 mRadioSuccessfullyInitialized = true; 215 } 216 217 @Override 218 public void onDestroy() { 219 if (Log.isLoggable(TAG, Log.DEBUG)) { 220 Log.d(TAG, "onDestroy()"); 221 } 222 223 close(); 224 225 if (mCarApi != null) { 226 mCarApi.disconnect(); 227 } 228 229 super.onDestroy(); 230 } 231 232 /** 233 * Opens the current radio band. Currently, this only supports FM and AM bands. 234 * 235 * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM}, 236 * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}. 237 * @return {@link RadioManager#STATUS_OK} if successful; otherwise, 238 * {@link RadioManager#STATUS_ERROR}. 239 */ 240 private int openRadioBandInternal(int radioBand) { 241 if (requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 242 Log.e(TAG, "openRadioBandInternal() audio focus request fail"); 243 return RadioManager.STATUS_ERROR; 244 } 245 246 mCurrentRadioBand = radioBand; 247 RadioManager.BandConfig config = getRadioConfig(radioBand); 248 249 if (config == null) { 250 Log.w(TAG, "Cannot create config for radio band: " + radioBand); 251 return RadioManager.STATUS_ERROR; 252 } 253 254 if (mRadioTuner != null) { 255 mRadioTuner.setConfiguration(config); 256 } else { 257 mRadioTuner = mRadioManager.openTuner(mModules.get(0).getId(), config, true, 258 mInternalRadioTunerCallback, null /* handler */); 259 } 260 261 if (Log.isLoggable(TAG, Log.DEBUG)) { 262 Log.d(TAG, "openRadioBandInternal() STATUS_OK"); 263 } 264 265 if (mBackgroundScanner != null) { 266 mBackgroundScanner.onRadioBandChanged(radioBand); 267 } 268 269 // Reset the counter for exponential backoff each time the radio tuner has been successfully 270 // opened. 271 mReOpenRadioTunerCount = 0; 272 273 return RadioManager.STATUS_OK; 274 } 275 276 /** 277 * Returns a {@link RadioRds} object that holds all the current radio metadata. If all the 278 * metadata is empty, then {@code null} is returned. 279 */ 280 @Nullable 281 private RadioRds createCurrentRadioRds() { 282 if (TextUtils.isEmpty(mCurrentChannelInfo) && TextUtils.isEmpty(mCurrentArtist) 283 && TextUtils.isEmpty(mCurrentSongTitle)) { 284 return null; 285 } 286 287 return new RadioRds(mCurrentChannelInfo, mCurrentArtist, mCurrentSongTitle); 288 } 289 290 /** 291 * Creates a {@link RadioStation} that encapsulates all the information about the current 292 * radio station. 293 */ 294 private RadioStation createCurrentRadioStation() { 295 // mCurrentRadioChannel can possibly be invalid if this class never receives a callback 296 // for onProgramInfoChanged(). As a result, manually retrieve the information for the 297 // current station from RadioTuner if this is the case. 298 if (mCurrentRadioChannel == RadioStorage.INVALID_RADIO_CHANNEL && mRadioTuner != null) { 299 if (Log.isLoggable(TAG, Log.DEBUG)) { 300 Log.d(TAG, "createCurrentRadioStation(); invalid current radio channel. " 301 + "Calling getProgramInformation for valid station"); 302 } 303 304 // getProgramInformation() expects an array of size 1. 305 RadioManager.ProgramInfo[] info = new RadioManager.ProgramInfo[1]; 306 int status = mRadioTuner.getProgramInformation(info); 307 308 if (Log.isLoggable(TAG, Log.DEBUG)) { 309 Log.d(TAG, "getProgramInformation() status: " + status + "; info: " + info[0]); 310 } 311 312 if (status == RadioManager.STATUS_OK && info[0] != null) { 313 mCurrentRadioChannel = info[0].getChannel(); 314 315 if (Log.isLoggable(TAG, Log.DEBUG)) { 316 Log.d(TAG, "program info channel: " + mCurrentRadioChannel); 317 } 318 } 319 } 320 321 return new RadioStation(mCurrentRadioChannel, 0 /* subChannelNumber */, 322 mCurrentRadioBand, createCurrentRadioRds()); 323 } 324 325 /** 326 * Returns the proper {@link android.hardware.radio.RadioManager.BandConfig} for the given 327 * radio band. {@code null} is returned if the band is not suppored. 328 */ 329 @Nullable 330 private RadioManager.BandConfig getRadioConfig(int selectedRadioBand) { 331 switch (selectedRadioBand) { 332 case RadioManager.BAND_AM: 333 case RadioManager.BAND_AM_HD: 334 return mAmConfig; 335 case RadioManager.BAND_FM: 336 case RadioManager.BAND_FM_HD: 337 return mFmConfig; 338 339 default: 340 return null; 341 } 342 } 343 344 private int requestAudioFocus() { 345 int status = AudioManager.AUDIOFOCUS_REQUEST_FAILED; 346 try { 347 status = mCarAudioManager.requestAudioFocus(this, mRadioAudioAttributes, 348 AudioManager.AUDIOFOCUS_GAIN, 0); 349 } catch (CarNotConnectedException e) { 350 Log.e(TAG, "requestAudioFocus() failed", e); 351 } 352 353 if (Log.isLoggable(TAG, Log.DEBUG)) { 354 Log.d(TAG, "requestAudioFocus status: " + status); 355 } 356 357 if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 358 mHasAudioFocus = true; 359 360 // Receiving audio focus means that the radio is un-muted. 361 for (IRadioCallback callback : mRadioTunerCallbacks) { 362 try { 363 callback.onRadioMuteChanged(false); 364 } catch (RemoteException e) { 365 Log.e(TAG, "requestAudioFocus(); onRadioMuteChanged() notify failed: " 366 + e.getMessage()); 367 } 368 } 369 } 370 371 return status; 372 } 373 374 private void abandonAudioFocus() { 375 if (Log.isLoggable(TAG, Log.DEBUG)) { 376 Log.d(TAG, "abandonAudioFocus()"); 377 } 378 379 if (mCarAudioManager == null) { 380 return; 381 } 382 383 mCarAudioManager.abandonAudioFocus(this, mRadioAudioAttributes); 384 mHasAudioFocus = false; 385 386 for (IRadioCallback callback : mRadioTunerCallbacks) { 387 try { 388 callback.onRadioMuteChanged(true); 389 } catch (RemoteException e) { 390 Log.e(TAG, "abandonAudioFocus(); onRadioMutechanged() notify failed: " 391 + e.getMessage()); 392 } 393 } 394 } 395 396 /** 397 * Closes any active {@link RadioTuner}s and releases audio focus. 398 */ 399 private void close() { 400 if (Log.isLoggable(TAG, Log.DEBUG)) { 401 Log.d(TAG, "close()"); 402 } 403 404 abandonAudioFocus(); 405 406 if (mRadioTuner != null) { 407 mRadioTuner.close(); 408 mRadioTuner = null; 409 } 410 } 411 412 @Override 413 public void onAudioFocusChange(int focusChange) { 414 if (Log.isLoggable(TAG, Log.DEBUG)) { 415 Log.d(TAG, "focus change: " + focusChange); 416 } 417 418 switch (focusChange) { 419 case AudioManager.AUDIOFOCUS_GAIN: 420 mHasAudioFocus = true; 421 openRadioBandInternal(mCurrentRadioBand); 422 break; 423 424 // For a transient loss, just allow the focus to be released. The radio will stop 425 // itself automatically. There is no need for an explicit abandon audio focus call 426 // because this removes the AudioFocusChangeListener. 427 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 428 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 429 mHasAudioFocus = false; 430 break; 431 432 case AudioManager.AUDIOFOCUS_LOSS: 433 close(); 434 break; 435 436 default: 437 // Do nothing for all other cases. 438 } 439 } 440 441 /** 442 * {@link CarConnectionCallback} that retrieves the {@link CarRadioManager}. 443 */ 444 private final CarConnectionCallback mCarConnectionCallback = 445 new CarConnectionCallback() { 446 @Override 447 public void onConnected(Car car) { 448 if (Log.isLoggable(TAG, Log.DEBUG)) { 449 Log.d(TAG, "Car service connected."); 450 } 451 try { 452 // The CarAudioManager only needs to be retrieved once. 453 if (mCarAudioManager == null) { 454 mCarAudioManager = (CarAudioManager) mCarApi.getCarManager( 455 android.car.Car.AUDIO_SERVICE); 456 457 mRadioAudioAttributes = mCarAudioManager.getAudioAttributesForCarUsage( 458 CarAudioManager.CAR_AUDIO_USAGE_RADIO); 459 } 460 } catch (CarNotConnectedException e) { 461 //TODO finish 462 Log.e(TAG, "Car not connected"); 463 } 464 } 465 466 @Override 467 public void onDisconnected(Car car) { 468 if (Log.isLoggable(TAG, Log.DEBUG)) { 469 Log.d(TAG, "Car service disconnected."); 470 } 471 } 472 }; 473 474 private IRadioManager.Stub mBinder = new IRadioManager.Stub() { 475 /** 476 * Tunes the radio to the given frequency. To be notified of a successful tune, register 477 * as a {@link android.hardware.radio.RadioTuner.Callback}. 478 */ 479 @Override 480 public void tune(RadioStation radioStation) { 481 if (mRadioManager == null || radioStation == null 482 || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 483 return; 484 } 485 486 if (mRadioTuner == null || radioStation.getRadioBand() != mCurrentRadioBand) { 487 int radioStatus = openRadioBandInternal(radioStation.getRadioBand()); 488 if (radioStatus == RadioManager.STATUS_ERROR) { 489 return; 490 } 491 } 492 493 int status = mRadioTuner.tune(radioStation.getChannelNumber(), 0 /* subChannel */); 494 495 if (Log.isLoggable(TAG, Log.DEBUG)) { 496 Log.d(TAG, "Tuning to station: " + radioStation + "\n\tstatus: " + status); 497 } 498 } 499 500 /** 501 * Seeks the radio forward. To be notified of a successful tune, register as a 502 * {@link android.hardware.radio.RadioTuner.Callback}. 503 */ 504 @Override 505 public void seekForward() { 506 if (mRadioManager == null 507 || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 508 return; 509 } 510 511 if (mRadioTuner == null) { 512 int radioStatus = openRadioBandInternal(mCurrentRadioBand); 513 if (radioStatus == RadioManager.STATUS_ERROR) { 514 return; 515 } 516 } 517 518 mRadioTuner.scan(RadioTuner.DIRECTION_UP, true); 519 } 520 521 /** 522 * Seeks the radio backwards. To be notified of a successful tune, register as a 523 * {@link android.hardware.radio.RadioTuner.Callback}. 524 */ 525 @Override 526 public void seekBackward() { 527 if (mRadioManager == null 528 || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 529 return; 530 } 531 532 if (mRadioTuner == null) { 533 int radioStatus = openRadioBandInternal(mCurrentRadioBand); 534 if (radioStatus == RadioManager.STATUS_ERROR) { 535 return; 536 } 537 } 538 539 mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, true); 540 } 541 542 /** 543 * Mutes the radio. 544 * 545 * @return {@code true} if the mute was successful. 546 */ 547 @Override 548 public boolean mute() { 549 if (mRadioManager == null) { 550 return false; 551 } 552 553 if (mCarAudioManager == null) { 554 if (Log.isLoggable(TAG, Log.DEBUG)) { 555 Log.d(TAG, "mute() called, but not connected to CarAudioManager"); 556 } 557 return false; 558 } 559 560 // If the radio does not currently have focus, then no need to do anything because the 561 // radio won't be playing any sound. 562 if (!mHasAudioFocus) { 563 if (Log.isLoggable(TAG, Log.DEBUG)) { 564 Log.d(TAG, "mute() called, but radio does not currently have audio focus; " 565 + "ignoring."); 566 } 567 return false; 568 } 569 570 boolean muteSuccessful = false; 571 572 try { 573 muteSuccessful = mCarAudioManager.setMediaMute(true); 574 575 if (Log.isLoggable(TAG, Log.DEBUG)) { 576 Log.d(TAG, "setMediaMute(true) status: " + muteSuccessful); 577 } 578 } catch (CarNotConnectedException e) { 579 Log.e(TAG, "mute() failed: " + e.getMessage()); 580 e.printStackTrace(); 581 } 582 583 if (muteSuccessful && mRadioTunerCallbacks.size() > 0) { 584 for (IRadioCallback callback : mRadioTunerCallbacks) { 585 try { 586 callback.onRadioMuteChanged(true); 587 } catch (RemoteException e) { 588 Log.e(TAG, "mute() notify failed: " + e.getMessage()); 589 } 590 } 591 } 592 593 return muteSuccessful; 594 } 595 596 /** 597 * Un-mutes the radio and causes audio to play. 598 * 599 * @return {@code true} if the un-mute was successful. 600 */ 601 @Override 602 public boolean unMute() { 603 if (mRadioManager == null) { 604 return false; 605 } 606 607 if (mCarAudioManager == null) { 608 if (Log.isLoggable(TAG, Log.DEBUG)) { 609 Log.d(TAG, "toggleMute() called, but not connected to CarAudioManager"); 610 } 611 return false; 612 } 613 614 // Requesting audio focus will automatically un-mute the radio if it had been muted. 615 return requestAudioFocus() == AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 616 } 617 618 /** 619 * Returns {@code true} if the radio is currently muted. 620 */ 621 @Override 622 public boolean isMuted() { 623 if (!mHasAudioFocus) { 624 return true; 625 } 626 627 if (mRadioManager == null) { 628 return true; 629 } 630 631 if (mCarAudioManager == null) { 632 if (Log.isLoggable(TAG, Log.DEBUG)) { 633 Log.d(TAG, "isMuted() called, but not connected to CarAudioManager"); 634 } 635 return true; 636 } 637 638 boolean isMuted = false; 639 640 try { 641 isMuted = mCarAudioManager.isMediaMuted(); 642 } catch (CarNotConnectedException e) { 643 Log.e(TAG, "isMuted() failed: " + e.getMessage()); 644 e.printStackTrace(); 645 } 646 647 return isMuted; 648 } 649 650 /** 651 * Opens the radio for the given band. 652 * 653 * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM}, 654 * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}. 655 * @return {@link RadioManager#STATUS_OK} if successful; otherwise, 656 * {@link RadioManager#STATUS_ERROR}. 657 */ 658 @Override 659 public int openRadioBand(int radioBand) { 660 if (Log.isLoggable(TAG, Log.DEBUG)) { 661 Log.d(TAG, "openRadioBand() for band: " + radioBand); 662 } 663 664 if (mRadioManager == null) { 665 return RadioManager.STATUS_ERROR; 666 } 667 668 return openRadioBandInternal(radioBand); 669 } 670 671 /** 672 * Adds the given {@link android.hardware.radio.RadioTuner.Callback} to be notified 673 * of any radio metadata changes. 674 */ 675 @Override 676 public void addRadioTunerCallback(IRadioCallback callback) { 677 if (callback == null) { 678 return; 679 } 680 681 mRadioTunerCallbacks.add(callback); 682 } 683 684 /** 685 * Removes the given {@link android.hardware.radio.RadioTuner.Callback} from receiving 686 * any radio metadata chagnes. 687 */ 688 @Override 689 public void removeRadioTunerCallback(IRadioCallback callback) { 690 if (callback == null) { 691 return; 692 } 693 694 mRadioTunerCallbacks.remove(callback); 695 } 696 697 /** 698 * Returns a {@link RadioStation} that encapsulates the information about the current 699 * station the radio is tuned to. 700 */ 701 @Override 702 public RadioStation getCurrentRadioStation() { 703 return createCurrentRadioStation(); 704 } 705 706 /** 707 * Returns {@code true} if the radio was able to successfully initialize. A value of 708 * {@code false} here could mean that the {@code RadioService} was not able to connect to 709 * the {@link RadioManager} or there were no radio modules on the current device. 710 */ 711 @Override 712 public boolean isInitialized() { 713 return mRadioSuccessfullyInitialized; 714 } 715 716 /** 717 * Returns {@code true} if the radio currently has focus and is therefore the application 718 * that is supplying music. 719 */ 720 @Override 721 public boolean hasFocus() { 722 return mHasAudioFocus; 723 } 724 725 /** 726 * Returns {@code true} if the current radio module has dual tuners, meaning that a tuner 727 * is available to scan for stations in the background. 728 */ 729 @Override 730 public boolean hasDualTuners() { 731 return mModules.size() >= 2; 732 } 733 }; 734 735 /** 736 * A extension of {@link android.hardware.radio.RadioTuner.Callback} that delegates to a 737 * callback registered on this service. 738 */ 739 private class InternalRadioCallback extends RadioTuner.Callback { 740 @Override 741 public void onProgramInfoChanged(RadioManager.ProgramInfo info) { 742 if (Log.isLoggable(TAG, Log.DEBUG)) { 743 Log.d(TAG, "onProgramInfoChanged(); info: " + info); 744 } 745 746 clearMetadata(); 747 748 if (info != null) { 749 mCurrentRadioChannel = info.getChannel(); 750 751 if (Log.isLoggable(TAG, Log.DEBUG)) { 752 Log.d(TAG, "onProgramInfoChanged(); info channel: " + mCurrentRadioChannel); 753 } 754 } 755 756 RadioStation station = createCurrentRadioStation(); 757 758 try { 759 for (IRadioCallback callback : mRadioTunerCallbacks) { 760 callback.onRadioStationChanged(station); 761 } 762 } catch (RemoteException e) { 763 Log.e(TAG, "onProgramInfoChanged(); " 764 + "Failed to notify IRadioCallbacks: " + e.getMessage()); 765 } 766 } 767 768 @Override 769 public void onMetadataChanged(RadioMetadata metadata) { 770 if (Log.isLoggable(TAG, Log.DEBUG)) { 771 Log.d(TAG, "onMetadataChanged(); metadata: " + metadata); 772 } 773 774 clearMetadata(); 775 updateMetadata(metadata); 776 777 RadioRds radioRds = createCurrentRadioRds(); 778 779 try { 780 for (IRadioCallback callback : mRadioTunerCallbacks) { 781 callback.onRadioMetadataChanged(radioRds); 782 } 783 } catch (RemoteException e) { 784 Log.e(TAG, "onMetadataChanged(); " 785 + "Failed to notify IRadioCallbacks: " + e.getMessage()); 786 } 787 } 788 789 @Override 790 public void onConfigurationChanged(RadioManager.BandConfig config) { 791 if (Log.isLoggable(TAG, Log.DEBUG)) { 792 Log.d(TAG, "onConfigurationChanged(): config: " + config); 793 } 794 795 clearMetadata(); 796 797 if (config != null) { 798 mCurrentRadioBand = config.getType(); 799 800 if (Log.isLoggable(TAG, Log.DEBUG)) { 801 Log.d(TAG, "onConfigurationChanged(): config type: " + mCurrentRadioBand); 802 } 803 804 } 805 806 try { 807 for (IRadioCallback callback : mRadioTunerCallbacks) { 808 callback.onRadioBandChanged(mCurrentRadioBand); 809 } 810 } catch (RemoteException e) { 811 Log.e(TAG, "onConfigurationChanged(); " 812 + "Failed to notify IRadioCallbacks: " + e.getMessage()); 813 } 814 } 815 816 @Override 817 public void onError(int status) { 818 Log.e(TAG, "onError(); status: " + status); 819 820 // If there is a hardware failure or the radio service died, then this requires a 821 // re-opening of the radio tuner. 822 if (status == RadioTuner.ERROR_HARDWARE_FAILURE 823 || status == RadioTuner.ERROR_SERVER_DIED) { 824 if (mRadioTuner != null) { 825 mRadioTuner.close(); 826 mRadioTuner = null; 827 } 828 829 // Attempt to re-open the RadioTuner. Each time the radio tuner fails to open, the 830 // mReOpenRadioTunerCount will be incremented. 831 mHandler.removeCallbacks(mOpenRadioTunerRunnable); 832 mHandler.postDelayed(mOpenRadioTunerRunnable, 833 mReOpenRadioTunerCount * RADIO_TUNER_REOPEN_DELAY_MS); 834 835 mReOpenRadioTunerCount++; 836 } 837 838 try { 839 for (IRadioCallback callback : mRadioTunerCallbacks) { 840 callback.onError(status); 841 } 842 } catch (RemoteException e) { 843 Log.e(TAG, "onError(); Failed to notify IRadioCallbacks: " + e.getMessage()); 844 } 845 } 846 847 @Override 848 public void onControlChanged(boolean control) { 849 // If the radio loses control of the RadioTuner, then close it and allow it to be 850 // re-opened when control has been gained. 851 if (!control) { 852 close(); 853 return; 854 } 855 856 if (mRadioTuner == null) { 857 openRadioBandInternal(mCurrentRadioBand); 858 } 859 } 860 861 /** 862 * Sets all metadata fields to {@code null}. 863 */ 864 private void clearMetadata() { 865 mCurrentChannelInfo = null; 866 mCurrentArtist = null; 867 mCurrentSongTitle = null; 868 } 869 870 /** 871 * Retrieves the relevant information off the given {@link RadioMetadata} object and 872 * sets them correspondingly on {@link #mCurrentChannelInfo}, {@link #mCurrentArtist} 873 * and {@link #mCurrentSongTitle}. 874 */ 875 private void updateMetadata(RadioMetadata metadata) { 876 if (metadata != null) { 877 mCurrentChannelInfo = metadata.getString(RadioMetadata.METADATA_KEY_RDS_PS); 878 mCurrentArtist = metadata.getString(RadioMetadata.METADATA_KEY_ARTIST); 879 mCurrentSongTitle = metadata.getString(RadioMetadata.METADATA_KEY_TITLE); 880 881 if (Log.isLoggable(TAG, Log.DEBUG)) { 882 Log.d(TAG, String.format("updateMetadata(): [channel info: %s, artist: %s, " 883 + "song title: %s]", mCurrentChannelInfo, mCurrentArtist, 884 mCurrentSongTitle)); 885 } 886 } 887 } 888 } 889 890 private final Runnable mOpenRadioTunerRunnable = () -> openRadioBandInternal(mCurrentRadioBand); 891 } 892