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.animation.ArgbEvaluator; 20 import android.animation.ValueAnimator; 21 import android.app.Activity; 22 import android.app.LoaderManager; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.Loader; 27 import android.content.ServiceConnection; 28 import android.graphics.Color; 29 import android.hardware.radio.RadioManager; 30 import android.hardware.radio.RadioTuner; 31 import android.media.AudioManager; 32 import android.os.Bundle; 33 import android.os.IBinder; 34 import android.os.RemoteException; 35 import android.support.annotation.ColorInt; 36 import android.support.annotation.Nullable; 37 import android.text.TextUtils; 38 import android.util.Log; 39 import android.view.View; 40 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 controller that handles the display of metadata on the current radio station. 51 */ 52 public class RadioController implements 53 RadioStorage.PresetsChangeListener, 54 RadioStorage.PreScannedChannelChangeListener, 55 LoaderManager.LoaderCallbacks<List<RadioStation>> { 56 private static final String TAG = "Em.RadioController"; 57 private static final int CHANNEL_LOADER_ID = 0; 58 59 /** 60 * The percentage by which to darken the color that should be set on the status bar. 61 * This darkening gives the status bar the illusion that it is transparent. 62 * 63 * @see RadioController#setShouldColorStatusBar(boolean) 64 */ 65 private static final float STATUS_BAR_DARKEN_PERCENTAGE = 0.4f; 66 67 /** 68 * The animation time for when the background of the radio shifts to a different color. 69 */ 70 private static final int BACKGROUND_CHANGE_ANIM_TIME_MS = 450; 71 private static final int INVALID_BACKGROUND_COLOR = 0; 72 73 private static final int CHANNEL_CHANGE_DURATION_MS = 200; 74 75 private int mCurrentChannelNumber = RadioStorage.INVALID_RADIO_CHANNEL; 76 77 private final Activity mActivity; 78 private IRadioManager mRadioManager; 79 80 private View mRadioBackground; 81 private boolean mShouldColorStatusBar; 82 83 /** 84 * An additional layer on top of the background that should match the color of 85 * {@link #mRadioBackground}. This view should only exist in the preset list. The reason this 86 * layer cannot be transparent is because it needs to be elevated, and elevation does not 87 * work if the background is undefined or transparent. 88 */ 89 private View mRadioPresetBackground; 90 91 private View mRadioErrorDisplay; 92 93 private final RadioChannelColorMapper mColorMapper; 94 @ColorInt private int mCurrentBackgroundColor = INVALID_BACKGROUND_COLOR; 95 96 private PrescannedRadioStationAdapter mAdapter; 97 private PreScannedChannelLoader mChannelLoader; 98 99 private final RadioDisplayController mRadioDisplayController; 100 private boolean mHasDualTuners; 101 102 /** 103 * Keeps track of if the user has manually muted the radio. This value is used to determine 104 * whether or not to un-mute the radio after an {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT} 105 * event has been received. 106 */ 107 private boolean mUserHasMuted; 108 109 private final RadioStorage mRadioStorage; 110 111 /** 112 * The current radio band. This value is one of the BAND_* values from {@link RadioManager}. 113 * For example, {@link RadioManager#BAND_FM}. 114 */ 115 private int mCurrentRadioBand = RadioStorage.INVALID_RADIO_BAND; 116 private final String mAmBandString; 117 private final String mFmBandString; 118 119 private RadioRds mCurrentRds; 120 121 private RadioStationChangeListener mStationChangeListener; 122 123 /** 124 * Interface for a class that will be notified when the current radio station has been changed. 125 */ 126 public interface RadioStationChangeListener { 127 /** 128 * Called when the current radio station has changed in the radio. 129 * 130 * @param station The current radio station. 131 */ 132 void onRadioStationChanged(RadioStation station); 133 } 134 135 public RadioController(Activity activity) { 136 mActivity = activity; 137 138 mRadioDisplayController = new RadioDisplayController(mActivity); 139 mColorMapper = RadioChannelColorMapper.getInstance(mActivity); 140 141 mAmBandString = mActivity.getString(R.string.radio_am_text); 142 mFmBandString = mActivity.getString(R.string.radio_fm_text); 143 144 mRadioStorage = RadioStorage.getInstance(mActivity); 145 mRadioStorage.addPresetsChangeListener(this); 146 } 147 148 /** 149 * Initializes this {@link RadioController} to control the UI whose root is the given container. 150 */ 151 public void initialize(View container) { 152 mCurrentBackgroundColor = INVALID_BACKGROUND_COLOR; 153 154 mRadioDisplayController.initialize(container); 155 156 mRadioDisplayController.setBackwardSeekButtonListener(mBackwardSeekClickListener); 157 mRadioDisplayController.setForwardSeekButtonListener(mForwardSeekClickListener); 158 mRadioDisplayController.setPlayButtonListener(mPlayPauseClickListener); 159 mRadioDisplayController.setAddPresetButtonListener(mPresetButtonClickListener); 160 161 mRadioBackground = container; 162 mRadioPresetBackground = container.findViewById(R.id.preset_current_card_container); 163 164 mRadioErrorDisplay = container.findViewById(R.id.radio_error_display); 165 166 updateRadioDisplay(); 167 } 168 169 /** 170 * Set whether or not this controller should also update the color of the status bar to match 171 * the current background color of the radio. The color that will be set on the status bar 172 * will be slightly darker, giving the illusion that the status bar is transparent. 173 * 174 * <p>This method is needed because of scene transitions. Scene transitions do not take into 175 * account padding that is added programmatically. Since there is no way to get the height of 176 * the status bar and set it in XML, it needs to be done in code. This breaks the scene 177 * transition. 178 * 179 * <p>To make this work, the status bar is not actually translucent; it is colored to appear 180 * that way via this method. 181 */ 182 public void setShouldColorStatusBar(boolean shouldColorStatusBar) { 183 mShouldColorStatusBar = shouldColorStatusBar; 184 } 185 186 /** 187 * Sets the listener that will be notified whenever the radio station changes. 188 */ 189 public void setRadioStationChangeListener(RadioStationChangeListener listener) { 190 mStationChangeListener = listener; 191 } 192 193 /** 194 * Starts the controller to handle radio tuning. This method should be called to begin 195 * radio playback. 196 */ 197 public void start() { 198 if (Log.isLoggable(TAG, Log.DEBUG)) { 199 Log.d(TAG, "starting radio"); 200 } 201 202 Intent bindIntent = new Intent(mActivity, RadioService.class); 203 if (!mActivity.bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) { 204 Log.e(TAG, "Failed to connect to RadioService."); 205 } 206 207 updateRadioDisplay(); 208 } 209 210 /** 211 * Retrieves information about the current radio station from {@link #mRadioManager} and updates 212 * the display of that information accordingly. 213 */ 214 private void updateRadioDisplay() { 215 if (mRadioManager == null) { 216 return; 217 } 218 219 try { 220 RadioStation station = mRadioManager.getCurrentRadioStation(); 221 222 if (Log.isLoggable(TAG, Log.DEBUG)) { 223 Log.d(TAG, "updateRadioDisplay(); current station: " + station); 224 } 225 226 mHasDualTuners = mRadioManager.hasDualTuners(); 227 228 if (mHasDualTuners) { 229 initializeDualTunerController(); 230 } else { 231 mRadioDisplayController.setSingleChannelDisplay(mRadioBackground); 232 } 233 234 // Update the AM/FM band display. 235 mCurrentRadioBand = station.getRadioBand(); 236 updateAmFmDisplayState(); 237 238 // Update the channel number. 239 setRadioChannel(station.getChannelNumber()); 240 241 // Ensure the play button properly reflects the current mute state. 242 mRadioDisplayController.setPlayPauseButtonState(mRadioManager.isMuted()); 243 244 mCallback.onRadioMetadataChanged(station.getRds()); 245 246 if (mStationChangeListener != null) { 247 mStationChangeListener.onRadioStationChanged(station); 248 } 249 } catch (RemoteException e) { 250 Log.e(TAG, "updateRadioDisplay(); remote exception: " + e.getMessage()); 251 } 252 } 253 254 /** 255 * Tunes the radio to the given channel if it is valid and a {@link RadioTuner} has been opened. 256 */ 257 public void tuneToRadioChannel(RadioStation radioStation) { 258 if (mRadioManager == null) { 259 return; 260 } 261 262 try { 263 mRadioManager.tune(radioStation); 264 } catch (RemoteException e) { 265 Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage()); 266 } 267 } 268 269 /** 270 * Returns the band this radio is currently tuned to. 271 */ 272 public int getCurrentRadioBand() { 273 return mCurrentRadioBand; 274 } 275 276 /** 277 * Returns the radio station that is currently playing on the radio. If this controller is 278 * not connected to the {@link RadioService} or a radio station cannot be retrieved, then 279 * {@code null} is returned. 280 */ 281 @Nullable 282 public RadioStation getCurrentRadioStation() { 283 if (mRadioManager == null) { 284 return null; 285 } 286 287 try { 288 return mRadioManager.getCurrentRadioStation(); 289 } catch (RemoteException e) { 290 Log.e(TAG, "getCurrentRadioStation(); error retrieving current station: " 291 + e.getMessage()); 292 } 293 294 return null; 295 } 296 297 /** 298 * Opens the given current radio band. Currently, this only supports FM and AM bands. 299 * 300 * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM}, 301 * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}. 302 */ 303 public void openRadioBand(int radioBand) { 304 if (mRadioManager == null || radioBand == mCurrentRadioBand) { 305 return; 306 } 307 308 // Reset the channel number so that we do not animate number changes between band changes. 309 mCurrentChannelNumber = RadioStorage.INVALID_RADIO_CHANNEL; 310 311 setCurrentRadioBand(radioBand); 312 mRadioStorage.storeRadioBand(mCurrentRadioBand); 313 314 try { 315 mRadioManager.openRadioBand(radioBand); 316 317 updateAmFmDisplayState(); 318 319 // Sets the initial mute state. This will resolve the mute state should be if an 320 // {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT} event is received followed by an 321 // {@link AudioManager#AUDIOFOCUS_GAIN} event. In this case, the radio will un-mute itself 322 // if the user has not muted beforehand. 323 if (mUserHasMuted) { 324 mRadioManager.mute(); 325 } 326 327 // Ensure the play button properly reflects the current mute state. 328 mRadioDisplayController.setPlayPauseButtonState(mRadioManager.isMuted()); 329 330 maybeTuneToStoredRadioChannel(); 331 } catch (RemoteException e) { 332 Log.e(TAG, "openRadioBand(); remote exception: " + e.getMessage()); 333 } 334 } 335 336 /** 337 * Attempts to tune to the last played radio channel for a particular band. For example, if 338 * the user switches to the AM band from FM, this method will attempt to tune to the last 339 * AM band that the user was on. 340 * 341 * <p>If a stored radio station cannot be found, then this method will initiate a seek so that 342 * the radio is always on a valid radio station. 343 */ 344 private void maybeTuneToStoredRadioChannel() { 345 mCurrentChannelNumber = mRadioStorage.getStoredRadioChannel(mCurrentRadioBand); 346 347 if (Log.isLoggable(TAG, Log.DEBUG)) { 348 Log.d(TAG, String.format("maybeTuneToStoredRadioChannel(); band: %s, channel %s", 349 mCurrentRadioBand, mCurrentChannelNumber)); 350 } 351 352 // Tune to a stored radio channel if it exists. 353 if (mCurrentChannelNumber != RadioStorage.INVALID_RADIO_CHANNEL) { 354 RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */, 355 mCurrentRadioBand, mCurrentRds); 356 tuneToRadioChannel(station); 357 } else { 358 // Otherwise, ensure that the radio is on a valid radio station (i.e. it will not 359 // start playing static) by initiating a seek. 360 try { 361 mRadioManager.seekForward(); 362 } catch (RemoteException e) { 363 Log.e(TAG, "maybeTuneToStoredRadioChannel(); remote exception: " + e.getMessage()); 364 } 365 } 366 } 367 368 /** 369 * Delegates to the {@link RadioDisplayController} to highlight the radio band that matches 370 * up to {@link #mCurrentRadioBand}. 371 */ 372 private void updateAmFmDisplayState() { 373 switch (mCurrentRadioBand) { 374 case RadioManager.BAND_FM: 375 mRadioDisplayController.setChannelBand(mFmBandString); 376 break; 377 378 case RadioManager.BAND_AM: 379 mRadioDisplayController.setChannelBand(mAmBandString); 380 break; 381 382 // TODO: Support BAND_FM_HD and BAND_AM_HD. 383 384 default: 385 mRadioDisplayController.setChannelBand(null); 386 } 387 } 388 389 /** 390 * Sets the radio channel to display. 391 * @param channel The radio channel frequency in Hz. 392 */ 393 private void setRadioChannel(int channel) { 394 if (Log.isLoggable(TAG, Log.DEBUG)) { 395 Log.d(TAG, "Setting radio channel: " + channel); 396 } 397 398 if (channel <= 0) { 399 mCurrentChannelNumber = channel; 400 mRadioDisplayController.setChannelNumber(""); 401 return; 402 } 403 404 if (mHasDualTuners) { 405 int position = mAdapter.getIndexOrInsertForStation(channel, mCurrentRadioBand); 406 mRadioDisplayController.setCurrentStationInList(position); 407 } 408 409 switch (mCurrentRadioBand) { 410 case RadioManager.BAND_FM: 411 setRadioChannelForFm(channel); 412 break; 413 414 case RadioManager.BAND_AM: 415 setRadioChannelForAm(channel); 416 break; 417 418 // TODO: Support BAND_FM_HD and BAND_AM_HD. 419 420 default: 421 // Do nothing and don't check presets, so return here. 422 return; 423 } 424 425 mCurrentChannelNumber = channel; 426 427 mRadioDisplayController.setChannelIsPreset( 428 mRadioStorage.isPreset(channel, mCurrentRadioBand)); 429 430 mRadioStorage.storeRadioChannel(mCurrentRadioBand, mCurrentChannelNumber); 431 432 maybeUpdateBackgroundColor(); 433 } 434 435 private void setRadioChannelForAm(int channel) { 436 // No need for animation if radio channel has never been set. 437 if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) { 438 mRadioDisplayController.setChannelNumber( 439 RadioChannelFormatter.AM_FORMATTER.format(channel)); 440 return; 441 } 442 443 animateRadioChannelChange(mCurrentChannelNumber, channel, mAmAnimatorListener); 444 } 445 446 private void setRadioChannelForFm(int channel) { 447 // FM channels are displayed in Khz. e.g. 88500 is displayed as 88.5. 448 float channelInKHz = (float) channel / 1000; 449 450 // No need for animation if radio channel has never been set. 451 if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) { 452 mRadioDisplayController.setChannelNumber( 453 RadioChannelFormatter.FM_FORMATTER.format(channelInKHz)); 454 return; 455 } 456 457 float startChannelNumber = (float) mCurrentChannelNumber / 1000; 458 animateRadioChannelChange(startChannelNumber, channelInKHz, mFmAnimatorListener); 459 } 460 461 /** 462 * Checks if the color of the radio background should be changed, and if so, animates that 463 * color change. 464 */ 465 private void maybeUpdateBackgroundColor() { 466 if (mRadioBackground == null) { 467 return; 468 } 469 470 int newColor = mColorMapper.getColorForStation(mCurrentRadioBand, mCurrentChannelNumber); 471 472 // No animation required if the colors are the same. 473 if (newColor == mCurrentBackgroundColor) { 474 return; 475 } 476 477 // If the current background color is invalid, then just set as the new color without any 478 // animation. 479 if (mCurrentBackgroundColor == INVALID_BACKGROUND_COLOR) { 480 mCurrentBackgroundColor = newColor; 481 setBackgroundColor(newColor); 482 } 483 484 // Otherwise, animate the background color change. 485 ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), 486 mCurrentBackgroundColor, newColor); 487 colorAnimation.setDuration(BACKGROUND_CHANGE_ANIM_TIME_MS); 488 colorAnimation.addUpdateListener(mBackgroundColorUpdater); 489 colorAnimation.start(); 490 491 mCurrentBackgroundColor = newColor; 492 } 493 494 private void setBackgroundColor(int backgroundColor) { 495 mRadioBackground.setBackgroundColor(backgroundColor); 496 497 if (mRadioPresetBackground != null) { 498 mRadioPresetBackground.setBackgroundColor(backgroundColor); 499 } 500 501 if (mShouldColorStatusBar) { 502 int red = darkenColor(Color.red(backgroundColor)); 503 int green = darkenColor(Color.green(backgroundColor)); 504 int blue = darkenColor(Color.blue(backgroundColor)); 505 int alpha = Color.alpha(backgroundColor); 506 507 mActivity.getWindow().setStatusBarColor( 508 Color.argb(alpha, red, green, blue)); 509 } 510 } 511 512 /** 513 * Darkens the given color by {@link #STATUS_BAR_DARKEN_PERCENTAGE}. 514 */ 515 private int darkenColor(int color) { 516 return (int) Math.max(color - (color * STATUS_BAR_DARKEN_PERCENTAGE), 0); 517 } 518 519 /** 520 * Animates the text in channel number from the given starting value to the given 521 * end value. 522 */ 523 private void animateRadioChannelChange(float startValue, float endValue, 524 ValueAnimator.AnimatorUpdateListener listener) { 525 ValueAnimator animator = new ValueAnimator(); 526 animator.setObjectValues(startValue, endValue); 527 animator.setDuration(CHANNEL_CHANGE_DURATION_MS); 528 animator.addUpdateListener(listener); 529 animator.start(); 530 } 531 532 /** 533 * Clears all metadata including song title, artist and station information. 534 */ 535 private void clearMetadataDisplay() { 536 mCurrentRds = null; 537 538 mRadioDisplayController.setCurrentSongArtistOrStation(null); 539 mRadioDisplayController.setCurrentSongTitle(null); 540 } 541 542 /** 543 * Sets the internal {@link #mCurrentRadioBand} to be the given radio band. Will also take care 544 * of restarting a load of the pre-scanned radio stations for the given band if there are dual 545 * tuners on the device. 546 */ 547 private void setCurrentRadioBand(int radioBand) { 548 if (mCurrentRadioBand == radioBand) { 549 return; 550 } 551 552 mCurrentRadioBand = radioBand; 553 554 if (mChannelLoader != null) { 555 mAdapter.setStations(new ArrayList<>()); 556 mChannelLoader.setCurrentRadioBand(radioBand); 557 mChannelLoader.forceLoad(); 558 } 559 } 560 561 /** 562 * Closes any active {@link RadioTuner}s and releases audio focus. 563 */ 564 private void close() { 565 if (Log.isLoggable(TAG, Log.DEBUG)) { 566 Log.d(TAG, "close()"); 567 } 568 569 // Lost focus, so display that the radio is not playing anymore. 570 mRadioDisplayController.setPlayPauseButtonState(true); 571 } 572 573 /** 574 * Closes all active connections in the {@link RadioController}. 575 */ 576 public void shutdown() { 577 if (Log.isLoggable(TAG, Log.DEBUG)) { 578 Log.d(TAG, "shutdown()"); 579 } 580 581 mActivity.unbindService(mServiceConnection); 582 mRadioStorage.removePresetsChangeListener(this); 583 mRadioStorage.removePreScannedChannelChangeListener(this); 584 585 if (mRadioManager != null) { 586 try { 587 mRadioManager.removeRadioTunerCallback(mCallback); 588 } catch (RemoteException e) { 589 Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage()); 590 } 591 } 592 593 close(); 594 } 595 596 /** 597 * Initializes all the extra components that are needed if this radio has dual tuners. 598 */ 599 private void initializeDualTunerController() { 600 if (Log.isLoggable(TAG, Log.DEBUG)) { 601 Log.d(TAG, "initializeDualTunerController()"); 602 } 603 604 mRadioStorage.addPreScannedChannelChangeListener(RadioController.this); 605 606 if (mAdapter == null) { 607 mAdapter = new PrescannedRadioStationAdapter(); 608 } 609 610 mRadioDisplayController.setChannelListDisplay(mRadioBackground, mAdapter); 611 612 // Initialize the loader that will load the pre-scanned channels for the current band. 613 mActivity.getLoaderManager().initLoader(CHANNEL_LOADER_ID, null /* args */, 614 RadioController.this /* callback */).forceLoad(); 615 } 616 617 @Override 618 public void onPresetsRefreshed() { 619 // Check if the current channel's preset status has changed. 620 mRadioDisplayController.setChannelIsPreset( 621 mRadioStorage.isPreset(mCurrentChannelNumber, mCurrentRadioBand)); 622 } 623 624 @Override 625 public void onPreScannedChannelChange(int radioBand) { 626 // If pre-scanned channels have changed for the current radio band, then refresh the list 627 // that is currently being displayed. 628 if (radioBand == mCurrentRadioBand && mChannelLoader != null) { 629 mChannelLoader.forceLoad(); 630 } 631 } 632 633 @Override 634 public Loader<List<RadioStation>> onCreateLoader(int id, Bundle args) { 635 // Only one loader, so no need to check for id. 636 mChannelLoader = new PreScannedChannelLoader(mActivity /* context */); 637 mChannelLoader.setCurrentRadioBand(mCurrentRadioBand); 638 639 return mChannelLoader; 640 } 641 642 @Override 643 public void onLoadFinished(Loader<List<RadioStation>> loader, 644 List<RadioStation> preScannedStations) { 645 if (Log.isLoggable(TAG, Log.DEBUG)) { 646 int size = preScannedStations == null ? 0 : preScannedStations.size(); 647 Log.d(TAG, "onLoadFinished(); number of pre-scanned stations: " + size); 648 } 649 650 if (Log.isLoggable(TAG, Log.VERBOSE) && preScannedStations != null) { 651 for (RadioStation station : preScannedStations) { 652 Log.v(TAG, "station: " + station.toString()); 653 } 654 } 655 656 mAdapter.setStations(preScannedStations); 657 658 int position = mAdapter.setStartingStation(mCurrentChannelNumber, mCurrentRadioBand); 659 mRadioDisplayController.setCurrentStationInList(position); 660 } 661 662 @Override 663 public void onLoaderReset(Loader<List<RadioStation>> loader) {} 664 665 /** 666 * Value animator for AM values. 667 */ 668 private ValueAnimator.AnimatorUpdateListener mAmAnimatorListener = 669 new ValueAnimator.AnimatorUpdateListener() { 670 public void onAnimationUpdate(ValueAnimator animation) { 671 mRadioDisplayController.setChannelNumber( 672 RadioChannelFormatter.AM_FORMATTER.format( 673 animation.getAnimatedValue())); 674 } 675 }; 676 677 /** 678 * Value animator for FM values. 679 */ 680 private ValueAnimator.AnimatorUpdateListener mFmAnimatorListener = 681 new ValueAnimator.AnimatorUpdateListener() { 682 public void onAnimationUpdate(ValueAnimator animation) { 683 mRadioDisplayController.setChannelNumber( 684 RadioChannelFormatter.FM_FORMATTER.format( 685 animation.getAnimatedValue())); 686 } 687 }; 688 689 private final IRadioCallback.Stub mCallback = new IRadioCallback.Stub() { 690 @Override 691 public void onRadioStationChanged(RadioStation station) { 692 if (Log.isLoggable(TAG, Log.DEBUG)) { 693 Log.d(TAG, "onRadioStationChanged: " + station); 694 } 695 696 if (station == null) { 697 return; 698 } 699 700 if (mCurrentChannelNumber != station.getChannelNumber()) { 701 setRadioChannel(station.getChannelNumber()); 702 } 703 704 onRadioMetadataChanged(station.getRds()); 705 706 // Notify that the current radio station has changed. 707 if (mStationChangeListener != null) { 708 try { 709 mStationChangeListener.onRadioStationChanged( 710 mRadioManager.getCurrentRadioStation()); 711 } catch (RemoteException e) { 712 Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage()); 713 } 714 } 715 } 716 717 /** 718 * Updates radio information based on the given {@link RadioRds}. 719 */ 720 @Override 721 public void onRadioMetadataChanged(RadioRds radioRds) { 722 if (Log.isLoggable(TAG, Log.DEBUG)) { 723 Log.d(TAG, "onMetadataChanged(); metadata: " + radioRds); 724 } 725 726 clearMetadataDisplay(); 727 728 if (radioRds == null) { 729 return; 730 } 731 732 mCurrentRds = radioRds; 733 734 if (Log.isLoggable(TAG, Log.DEBUG)) { 735 Log.d(TAG, "mCurrentRds: " + mCurrentRds); 736 } 737 738 String programService = radioRds.getProgramService(); 739 String artistMetadata = radioRds.getSongArtist(); 740 741 mRadioDisplayController.setCurrentSongArtistOrStation( 742 TextUtils.isEmpty(artistMetadata) ? programService : artistMetadata); 743 mRadioDisplayController.setCurrentSongTitle(radioRds.getSongTitle()); 744 745 // Since new metadata exists, update the preset that is stored in the database if 746 // it exists. 747 if (TextUtils.isEmpty(programService)) { 748 return; 749 } 750 751 RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */, 752 mCurrentRadioBand, radioRds); 753 boolean isPreset = mRadioStorage.isPreset(station); 754 755 if (isPreset) { 756 if (Log.isLoggable(TAG, Log.DEBUG)) { 757 Log.d(TAG, "Current channel is a preset; updating metadata in the database."); 758 } 759 760 mRadioStorage.storePreset(station); 761 } 762 } 763 764 @Override 765 public void onRadioBandChanged(int radioBand) { 766 if (Log.isLoggable(TAG, Log.DEBUG)) { 767 Log.d(TAG, "onRadioBandChanged: " + radioBand); 768 } 769 770 setCurrentRadioBand(radioBand); 771 updateAmFmDisplayState(); 772 773 // Check that the radio channel is being correctly formatted. 774 setRadioChannel(mCurrentChannelNumber); 775 } 776 777 @Override 778 public void onRadioMuteChanged(boolean isMuted) { 779 mRadioDisplayController.setPlayPauseButtonState(isMuted); 780 } 781 782 @Override 783 public void onError(int status) { 784 Log.e(TAG, "Radio callback error with status: " + status); 785 close(); 786 } 787 }; 788 789 private final View.OnClickListener mBackwardSeekClickListener = new View.OnClickListener() { 790 @Override 791 public void onClick(View v) { 792 if (mRadioManager == null) { 793 return; 794 } 795 796 clearMetadataDisplay(); 797 798 if (!mHasDualTuners) { 799 try { 800 mRadioManager.seekBackward(); 801 } catch (RemoteException e) { 802 Log.e(TAG, "backwardSeek(); remote exception: " + e.getMessage()); 803 } 804 return; 805 } 806 807 RadioStation prevStation = mAdapter.getPrevStation(); 808 809 if (prevStation != null) { 810 if (Log.isLoggable(TAG, Log.DEBUG)) { 811 Log.d(TAG, "Seek backwards to station: " + prevStation); 812 } 813 814 // Tune to the previous station, and then update the UI to reflect that tune. 815 try { 816 mRadioManager.tune(prevStation); 817 } catch (RemoteException e) { 818 Log.e(TAG, "backwardSeek(); remote exception: " + e.getMessage()); 819 } 820 821 int position = mAdapter.getCurrentPosition(); 822 mRadioDisplayController.setCurrentStationInList(position); 823 } 824 } 825 }; 826 827 private final View.OnClickListener mForwardSeekClickListener = new View.OnClickListener() { 828 @Override 829 public void onClick(View v) { 830 if (mRadioManager == null) { 831 return; 832 } 833 834 clearMetadataDisplay(); 835 836 if (!mHasDualTuners) { 837 try { 838 mRadioManager.seekForward(); 839 } catch (RemoteException e) { 840 Log.e(TAG, "forwardSeek(); remote exception: " + e.getMessage()); 841 } 842 return; 843 } 844 845 RadioStation nextStation = mAdapter.getNextStation(); 846 847 if (nextStation != null) { 848 if (Log.isLoggable(TAG, Log.DEBUG)) { 849 Log.d(TAG, "Seek forward to station: " + nextStation); 850 } 851 852 // Tune to the next station, and then update the UI to reflect that tune. 853 try { 854 mRadioManager.tune(nextStation); 855 } catch (RemoteException e) { 856 Log.e(TAG, "forwardSeek(); remote exception: " + e.getMessage()); 857 } 858 859 int position = mAdapter.getCurrentPosition(); 860 mRadioDisplayController.setCurrentStationInList(position); 861 } 862 } 863 }; 864 865 /** 866 * Click listener for the play/pause button. Currently, all this does is mute/unmute the radio 867 * because the {@link RadioManager} does not support the ability to pause/start again. 868 */ 869 private final View.OnClickListener mPlayPauseClickListener = new View.OnClickListener() { 870 @Override 871 public void onClick(View v) { 872 if (mRadioManager == null) { 873 return; 874 } 875 876 try { 877 if (Log.isLoggable(TAG, Log.DEBUG)) { 878 Log.d(TAG, "Play button clicked. Currently muted: " + mRadioManager.isMuted()); 879 } 880 881 if (mRadioManager.isMuted()) { 882 mRadioManager.unMute(); 883 } else { 884 mRadioManager.mute(); 885 } 886 887 boolean isMuted = mRadioManager.isMuted(); 888 889 mUserHasMuted = isMuted; 890 mRadioDisplayController.setPlayPauseButtonState(isMuted); 891 } catch (RemoteException e) { 892 Log.e(TAG, "playPauseClickListener(); remote exception: " + e.getMessage()); 893 } 894 } 895 }; 896 897 private final View.OnClickListener mPresetButtonClickListener = new View.OnClickListener() { 898 // TODO: Maybe add a check to send a store/remove preset event after a delay so that 899 // there aren't multiple writes if the user presses the button quickly. 900 @Override 901 public void onClick(View v) { 902 if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) { 903 if (Log.isLoggable(TAG, Log.DEBUG)) { 904 Log.d(TAG, "Attempting to store invalid radio station as a preset. Ignoring"); 905 } 906 907 return; 908 } 909 910 RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */, 911 mCurrentRadioBand, mCurrentRds); 912 boolean isPreset = mRadioStorage.isPreset(station); 913 914 if (Log.isLoggable(TAG, Log.DEBUG)) { 915 Log.d(TAG, "Toggling preset for " + station 916 + "\n\tIs currently a preset: " + isPreset); 917 } 918 919 if (isPreset) { 920 mRadioStorage.removePreset(station); 921 } else { 922 mRadioStorage.storePreset(station); 923 } 924 925 // Update the UI immediately. If the preset failed for some reason, the RadioStorage 926 // will notify us and UI update will happen then. 927 mRadioDisplayController.setChannelIsPreset(!isPreset); 928 } 929 }; 930 931 private ServiceConnection mServiceConnection = new ServiceConnection() { 932 @Override 933 public void onServiceConnected(ComponentName className, IBinder binder) { 934 mRadioManager = ((IRadioManager) binder); 935 936 try { 937 if (mRadioManager == null || !mRadioManager.isInitialized()) { 938 mRadioDisplayController.setEnabled(false); 939 940 if (mRadioErrorDisplay != null) { 941 mRadioErrorDisplay.setVisibility(View.VISIBLE); 942 } 943 944 return; 945 } 946 947 mRadioDisplayController.setEnabled(true); 948 949 if (mRadioErrorDisplay != null) { 950 mRadioErrorDisplay.setVisibility(View.GONE); 951 } 952 953 mHasDualTuners = mRadioManager.hasDualTuners(); 954 955 if (mHasDualTuners) { 956 initializeDualTunerController(); 957 } else { 958 mRadioDisplayController.setSingleChannelDisplay(mRadioBackground); 959 } 960 961 mRadioManager.addRadioTunerCallback(mCallback); 962 963 int radioBand = mRadioStorage.getStoredRadioBand(); 964 965 // Upon successful connection, open the radio. 966 openRadioBand(radioBand); 967 maybeTuneToStoredRadioChannel(); 968 969 if (mStationChangeListener != null) { 970 mStationChangeListener.onRadioStationChanged( 971 mRadioManager.getCurrentRadioStation()); 972 } 973 } catch (RemoteException e) { 974 Log.e(TAG, "onServiceConnected(); remote exception: " + e.getMessage()); 975 } 976 } 977 978 @Override 979 public void onServiceDisconnected(ComponentName className) { 980 mRadioManager = null; 981 } 982 }; 983 984 private final ValueAnimator.AnimatorUpdateListener mBackgroundColorUpdater = 985 animator -> { 986 int backgroundColor = (int) animator.getAnimatedValue(); 987 setBackgroundColor(backgroundColor); 988 }; 989 } 990