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