| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.car.radio; |
| |
| import android.animation.ArgbEvaluator; |
| import android.animation.ValueAnimator; |
| import android.app.Activity; |
| import android.app.LoaderManager; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.Loader; |
| import android.content.ServiceConnection; |
| import android.graphics.Color; |
| import android.hardware.radio.RadioManager; |
| import android.hardware.radio.RadioTuner; |
| import android.media.AudioManager; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.support.annotation.ColorInt; |
| import android.support.annotation.Nullable; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.View; |
| |
| import com.android.car.radio.service.IRadioCallback; |
| import com.android.car.radio.service.IRadioManager; |
| import com.android.car.radio.service.RadioRds; |
| import com.android.car.radio.service.RadioStation; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * A controller that handles the display of metadata on the current radio station. |
| */ |
| public class RadioController implements |
| RadioStorage.PresetsChangeListener, |
| RadioStorage.PreScannedChannelChangeListener, |
| LoaderManager.LoaderCallbacks<List<RadioStation>> { |
| private static final String TAG = "Em.RadioController"; |
| private static final int CHANNEL_LOADER_ID = 0; |
| |
| /** |
| * The percentage by which to darken the color that should be set on the status bar. |
| * This darkening gives the status bar the illusion that it is transparent. |
| * |
| * @see RadioController#setShouldColorStatusBar(boolean) |
| */ |
| private static final float STATUS_BAR_DARKEN_PERCENTAGE = 0.4f; |
| |
| /** |
| * The animation time for when the background of the radio shifts to a different color. |
| */ |
| private static final int BACKGROUND_CHANGE_ANIM_TIME_MS = 450; |
| private static final int INVALID_BACKGROUND_COLOR = 0; |
| |
| private static final int CHANNEL_CHANGE_DURATION_MS = 200; |
| |
| private int mCurrentChannelNumber = RadioStorage.INVALID_RADIO_CHANNEL; |
| |
| private final Activity mActivity; |
| private IRadioManager mRadioManager; |
| |
| private View mRadioBackground; |
| private boolean mShouldColorStatusBar; |
| |
| /** |
| * An additional layer on top of the background that should match the color of |
| * {@link #mRadioBackground}. This view should only exist in the preset list. The reason this |
| * layer cannot be transparent is because it needs to be elevated, and elevation does not |
| * work if the background is undefined or transparent. |
| */ |
| private View mRadioPresetBackground; |
| |
| private View mRadioErrorDisplay; |
| |
| private final RadioChannelColorMapper mColorMapper; |
| @ColorInt private int mCurrentBackgroundColor = INVALID_BACKGROUND_COLOR; |
| |
| private PrescannedRadioStationAdapter mAdapter; |
| private PreScannedChannelLoader mChannelLoader; |
| |
| private final RadioDisplayController mRadioDisplayController; |
| private boolean mHasDualTuners; |
| |
| /** |
| * Keeps track of if the user has manually muted the radio. This value is used to determine |
| * whether or not to un-mute the radio after an {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT} |
| * event has been received. |
| */ |
| private boolean mUserHasMuted; |
| |
| private final RadioStorage mRadioStorage; |
| |
| /** |
| * The current radio band. This value is one of the BAND_* values from {@link RadioManager}. |
| * For example, {@link RadioManager#BAND_FM}. |
| */ |
| private int mCurrentRadioBand = RadioStorage.INVALID_RADIO_BAND; |
| private final String mAmBandString; |
| private final String mFmBandString; |
| |
| private RadioRds mCurrentRds; |
| |
| private RadioStationChangeListener mStationChangeListener; |
| |
| /** |
| * Interface for a class that will be notified when the current radio station has been changed. |
| */ |
| public interface RadioStationChangeListener { |
| /** |
| * Called when the current radio station has changed in the radio. |
| * |
| * @param station The current radio station. |
| */ |
| void onRadioStationChanged(RadioStation station); |
| } |
| |
| public RadioController(Activity activity) { |
| mActivity = activity; |
| |
| mRadioDisplayController = new RadioDisplayController(mActivity); |
| mColorMapper = RadioChannelColorMapper.getInstance(mActivity); |
| |
| mAmBandString = mActivity.getString(R.string.radio_am_text); |
| mFmBandString = mActivity.getString(R.string.radio_fm_text); |
| |
| mRadioStorage = RadioStorage.getInstance(mActivity); |
| mRadioStorage.addPresetsChangeListener(this); |
| } |
| |
| /** |
| * Initializes this {@link RadioController} to control the UI whose root is the given container. |
| */ |
| public void initialize(View container) { |
| mCurrentBackgroundColor = INVALID_BACKGROUND_COLOR; |
| |
| mRadioDisplayController.initialize(container); |
| |
| mRadioDisplayController.setBackwardSeekButtonListener(mBackwardSeekClickListener); |
| mRadioDisplayController.setForwardSeekButtonListener(mForwardSeekClickListener); |
| mRadioDisplayController.setPlayButtonListener(mPlayPauseClickListener); |
| mRadioDisplayController.setAddPresetButtonListener(mPresetButtonClickListener); |
| |
| mRadioBackground = container; |
| mRadioPresetBackground = container.findViewById(R.id.preset_current_card_container); |
| |
| mRadioErrorDisplay = container.findViewById(R.id.radio_error_display); |
| |
| updateRadioDisplay(); |
| } |
| |
| /** |
| * Set whether or not this controller should also update the color of the status bar to match |
| * the current background color of the radio. The color that will be set on the status bar |
| * will be slightly darker, giving the illusion that the status bar is transparent. |
| * |
| * <p>This method is needed because of scene transitions. Scene transitions do not take into |
| * account padding that is added programmatically. Since there is no way to get the height of |
| * the status bar and set it in XML, it needs to be done in code. This breaks the scene |
| * transition. |
| * |
| * <p>To make this work, the status bar is not actually translucent; it is colored to appear |
| * that way via this method. |
| */ |
| public void setShouldColorStatusBar(boolean shouldColorStatusBar) { |
| mShouldColorStatusBar = shouldColorStatusBar; |
| } |
| |
| /** |
| * Sets the listener that will be notified whenever the radio station changes. |
| */ |
| public void setRadioStationChangeListener(RadioStationChangeListener listener) { |
| mStationChangeListener = listener; |
| } |
| |
| /** |
| * Starts the controller to handle radio tuning. This method should be called to begin |
| * radio playback. |
| */ |
| public void start() { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "starting radio"); |
| } |
| |
| Intent bindIntent = new Intent(mActivity, RadioService.class); |
| if (!mActivity.bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) { |
| Log.e(TAG, "Failed to connect to RadioService."); |
| } |
| |
| updateRadioDisplay(); |
| } |
| |
| /** |
| * Retrieves information about the current radio station from {@link #mRadioManager} and updates |
| * the display of that information accordingly. |
| */ |
| private void updateRadioDisplay() { |
| if (mRadioManager == null) { |
| return; |
| } |
| |
| try { |
| RadioStation station = mRadioManager.getCurrentRadioStation(); |
| |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "updateRadioDisplay(); current station: " + station); |
| } |
| |
| mHasDualTuners = mRadioManager.hasDualTuners(); |
| |
| if (mHasDualTuners) { |
| initializeDualTunerController(); |
| } else { |
| mRadioDisplayController.setSingleChannelDisplay(mRadioBackground); |
| } |
| |
| // Update the AM/FM band display. |
| mCurrentRadioBand = station.getRadioBand(); |
| updateAmFmDisplayState(); |
| |
| // Update the channel number. |
| setRadioChannel(station.getChannelNumber()); |
| |
| // Ensure the play button properly reflects the current mute state. |
| mRadioDisplayController.setPlayPauseButtonState(mRadioManager.isMuted()); |
| |
| mCallback.onRadioMetadataChanged(station.getRds()); |
| |
| if (mStationChangeListener != null) { |
| mStationChangeListener.onRadioStationChanged(station); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "updateRadioDisplay(); remote exception: " + e.getMessage()); |
| } |
| } |
| |
| /** |
| * Tunes the radio to the given channel if it is valid and a {@link RadioTuner} has been opened. |
| */ |
| public void tuneToRadioChannel(RadioStation radioStation) { |
| if (mRadioManager == null) { |
| return; |
| } |
| |
| try { |
| mRadioManager.tune(radioStation); |
| } catch (RemoteException e) { |
| Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage()); |
| } |
| } |
| |
| /** |
| * Returns the band this radio is currently tuned to. |
| */ |
| public int getCurrentRadioBand() { |
| return mCurrentRadioBand; |
| } |
| |
| /** |
| * Returns the radio station that is currently playing on the radio. If this controller is |
| * not connected to the {@link RadioService} or a radio station cannot be retrieved, then |
| * {@code null} is returned. |
| */ |
| @Nullable |
| public RadioStation getCurrentRadioStation() { |
| if (mRadioManager == null) { |
| return null; |
| } |
| |
| try { |
| return mRadioManager.getCurrentRadioStation(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "getCurrentRadioStation(); error retrieving current station: " |
| + e.getMessage()); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Opens the given current radio band. Currently, this only supports FM and AM bands. |
| * |
| * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM}, |
| * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}. |
| */ |
| public void openRadioBand(int radioBand) { |
| if (mRadioManager == null || radioBand == mCurrentRadioBand) { |
| return; |
| } |
| |
| // Reset the channel number so that we do not animate number changes between band changes. |
| mCurrentChannelNumber = RadioStorage.INVALID_RADIO_CHANNEL; |
| |
| setCurrentRadioBand(radioBand); |
| mRadioStorage.storeRadioBand(mCurrentRadioBand); |
| |
| try { |
| mRadioManager.openRadioBand(radioBand); |
| |
| updateAmFmDisplayState(); |
| |
| // Sets the initial mute state. This will resolve the mute state should be if an |
| // {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT} event is received followed by an |
| // {@link AudioManager#AUDIOFOCUS_GAIN} event. In this case, the radio will un-mute itself |
| // if the user has not muted beforehand. |
| if (mUserHasMuted) { |
| mRadioManager.mute(); |
| } |
| |
| // Ensure the play button properly reflects the current mute state. |
| mRadioDisplayController.setPlayPauseButtonState(mRadioManager.isMuted()); |
| |
| maybeTuneToStoredRadioChannel(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "openRadioBand(); remote exception: " + e.getMessage()); |
| } |
| } |
| |
| /** |
| * Attempts to tune to the last played radio channel for a particular band. For example, if |
| * the user switches to the AM band from FM, this method will attempt to tune to the last |
| * AM band that the user was on. |
| * |
| * <p>If a stored radio station cannot be found, then this method will initiate a seek so that |
| * the radio is always on a valid radio station. |
| */ |
| private void maybeTuneToStoredRadioChannel() { |
| mCurrentChannelNumber = mRadioStorage.getStoredRadioChannel(mCurrentRadioBand); |
| |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, String.format("maybeTuneToStoredRadioChannel(); band: %s, channel %s", |
| mCurrentRadioBand, mCurrentChannelNumber)); |
| } |
| |
| // Tune to a stored radio channel if it exists. |
| if (mCurrentChannelNumber != RadioStorage.INVALID_RADIO_CHANNEL) { |
| RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */, |
| mCurrentRadioBand, mCurrentRds); |
| tuneToRadioChannel(station); |
| } else { |
| // Otherwise, ensure that the radio is on a valid radio station (i.e. it will not |
| // start playing static) by initiating a seek. |
| try { |
| mRadioManager.seekForward(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "maybeTuneToStoredRadioChannel(); remote exception: " + e.getMessage()); |
| } |
| } |
| } |
| |
| /** |
| * Delegates to the {@link RadioDisplayController} to highlight the radio band that matches |
| * up to {@link #mCurrentRadioBand}. |
| */ |
| private void updateAmFmDisplayState() { |
| switch (mCurrentRadioBand) { |
| case RadioManager.BAND_FM: |
| mRadioDisplayController.setChannelBand(mFmBandString); |
| break; |
| |
| case RadioManager.BAND_AM: |
| mRadioDisplayController.setChannelBand(mAmBandString); |
| break; |
| |
| // TODO: Support BAND_FM_HD and BAND_AM_HD. |
| |
| default: |
| mRadioDisplayController.setChannelBand(null); |
| } |
| } |
| |
| /** |
| * Sets the radio channel to display. |
| * @param channel The radio channel frequency in Hz. |
| */ |
| private void setRadioChannel(int channel) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Setting radio channel: " + channel); |
| } |
| |
| if (channel <= 0) { |
| mCurrentChannelNumber = channel; |
| mRadioDisplayController.setChannelNumber(""); |
| return; |
| } |
| |
| if (mHasDualTuners) { |
| int position = mAdapter.getIndexOrInsertForStation(channel, mCurrentRadioBand); |
| mRadioDisplayController.setCurrentStationInList(position); |
| } |
| |
| switch (mCurrentRadioBand) { |
| case RadioManager.BAND_FM: |
| setRadioChannelForFm(channel); |
| break; |
| |
| case RadioManager.BAND_AM: |
| setRadioChannelForAm(channel); |
| break; |
| |
| // TODO: Support BAND_FM_HD and BAND_AM_HD. |
| |
| default: |
| // Do nothing and don't check presets, so return here. |
| return; |
| } |
| |
| mCurrentChannelNumber = channel; |
| |
| mRadioDisplayController.setChannelIsPreset( |
| mRadioStorage.isPreset(channel, mCurrentRadioBand)); |
| |
| mRadioStorage.storeRadioChannel(mCurrentRadioBand, mCurrentChannelNumber); |
| |
| maybeUpdateBackgroundColor(); |
| } |
| |
| private void setRadioChannelForAm(int channel) { |
| // No need for animation if radio channel has never been set. |
| if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) { |
| mRadioDisplayController.setChannelNumber( |
| RadioChannelFormatter.AM_FORMATTER.format(channel)); |
| return; |
| } |
| |
| animateRadioChannelChange(mCurrentChannelNumber, channel, mAmAnimatorListener); |
| } |
| |
| private void setRadioChannelForFm(int channel) { |
| // FM channels are displayed in Khz. e.g. 88500 is displayed as 88.5. |
| float channelInKHz = (float) channel / 1000; |
| |
| // No need for animation if radio channel has never been set. |
| if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) { |
| mRadioDisplayController.setChannelNumber( |
| RadioChannelFormatter.FM_FORMATTER.format(channelInKHz)); |
| return; |
| } |
| |
| float startChannelNumber = (float) mCurrentChannelNumber / 1000; |
| animateRadioChannelChange(startChannelNumber, channelInKHz, mFmAnimatorListener); |
| } |
| |
| /** |
| * Checks if the color of the radio background should be changed, and if so, animates that |
| * color change. |
| */ |
| private void maybeUpdateBackgroundColor() { |
| if (mRadioBackground == null) { |
| return; |
| } |
| |
| int newColor = mColorMapper.getColorForStation(mCurrentRadioBand, mCurrentChannelNumber); |
| |
| // No animation required if the colors are the same. |
| if (newColor == mCurrentBackgroundColor) { |
| return; |
| } |
| |
| // If the current background color is invalid, then just set as the new color without any |
| // animation. |
| if (mCurrentBackgroundColor == INVALID_BACKGROUND_COLOR) { |
| mCurrentBackgroundColor = newColor; |
| setBackgroundColor(newColor); |
| } |
| |
| // Otherwise, animate the background color change. |
| ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), |
| mCurrentBackgroundColor, newColor); |
| colorAnimation.setDuration(BACKGROUND_CHANGE_ANIM_TIME_MS); |
| colorAnimation.addUpdateListener(mBackgroundColorUpdater); |
| colorAnimation.start(); |
| |
| mCurrentBackgroundColor = newColor; |
| } |
| |
| private void setBackgroundColor(int backgroundColor) { |
| mRadioBackground.setBackgroundColor(backgroundColor); |
| |
| if (mRadioPresetBackground != null) { |
| mRadioPresetBackground.setBackgroundColor(backgroundColor); |
| } |
| |
| if (mShouldColorStatusBar) { |
| int red = darkenColor(Color.red(backgroundColor)); |
| int green = darkenColor(Color.green(backgroundColor)); |
| int blue = darkenColor(Color.blue(backgroundColor)); |
| int alpha = Color.alpha(backgroundColor); |
| |
| mActivity.getWindow().setStatusBarColor( |
| Color.argb(alpha, red, green, blue)); |
| } |
| } |
| |
| /** |
| * Darkens the given color by {@link #STATUS_BAR_DARKEN_PERCENTAGE}. |
| */ |
| private int darkenColor(int color) { |
| return (int) Math.max(color - (color * STATUS_BAR_DARKEN_PERCENTAGE), 0); |
| } |
| |
| /** |
| * Animates the text in channel number from the given starting value to the given |
| * end value. |
| */ |
| private void animateRadioChannelChange(float startValue, float endValue, |
| ValueAnimator.AnimatorUpdateListener listener) { |
| ValueAnimator animator = new ValueAnimator(); |
| animator.setObjectValues(startValue, endValue); |
| animator.setDuration(CHANNEL_CHANGE_DURATION_MS); |
| animator.addUpdateListener(listener); |
| animator.start(); |
| } |
| |
| /** |
| * Clears all metadata including song title, artist and station information. |
| */ |
| private void clearMetadataDisplay() { |
| mCurrentRds = null; |
| |
| mRadioDisplayController.setCurrentSongArtistOrStation(null); |
| mRadioDisplayController.setCurrentSongTitle(null); |
| } |
| |
| /** |
| * Sets the internal {@link #mCurrentRadioBand} to be the given radio band. Will also take care |
| * of restarting a load of the pre-scanned radio stations for the given band if there are dual |
| * tuners on the device. |
| */ |
| private void setCurrentRadioBand(int radioBand) { |
| if (mCurrentRadioBand == radioBand) { |
| return; |
| } |
| |
| mCurrentRadioBand = radioBand; |
| |
| if (mChannelLoader != null) { |
| mAdapter.setStations(new ArrayList<>()); |
| mChannelLoader.setCurrentRadioBand(radioBand); |
| mChannelLoader.forceLoad(); |
| } |
| } |
| |
| /** |
| * Closes any active {@link RadioTuner}s and releases audio focus. |
| */ |
| private void close() { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "close()"); |
| } |
| |
| // Lost focus, so display that the radio is not playing anymore. |
| mRadioDisplayController.setPlayPauseButtonState(true); |
| } |
| |
| /** |
| * Closes all active connections in the {@link RadioController}. |
| */ |
| public void shutdown() { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "shutdown()"); |
| } |
| |
| mActivity.unbindService(mServiceConnection); |
| mRadioStorage.removePresetsChangeListener(this); |
| mRadioStorage.removePreScannedChannelChangeListener(this); |
| |
| if (mRadioManager != null) { |
| try { |
| mRadioManager.removeRadioTunerCallback(mCallback); |
| } catch (RemoteException e) { |
| Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage()); |
| } |
| } |
| |
| close(); |
| } |
| |
| /** |
| * Initializes all the extra components that are needed if this radio has dual tuners. |
| */ |
| private void initializeDualTunerController() { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "initializeDualTunerController()"); |
| } |
| |
| mRadioStorage.addPreScannedChannelChangeListener(RadioController.this); |
| |
| if (mAdapter == null) { |
| mAdapter = new PrescannedRadioStationAdapter(); |
| } |
| |
| mRadioDisplayController.setChannelListDisplay(mRadioBackground, mAdapter); |
| |
| // Initialize the loader that will load the pre-scanned channels for the current band. |
| mActivity.getLoaderManager().initLoader(CHANNEL_LOADER_ID, null /* args */, |
| RadioController.this /* callback */).forceLoad(); |
| } |
| |
| @Override |
| public void onPresetsRefreshed() { |
| // Check if the current channel's preset status has changed. |
| mRadioDisplayController.setChannelIsPreset( |
| mRadioStorage.isPreset(mCurrentChannelNumber, mCurrentRadioBand)); |
| } |
| |
| @Override |
| public void onPreScannedChannelChange(int radioBand) { |
| // If pre-scanned channels have changed for the current radio band, then refresh the list |
| // that is currently being displayed. |
| if (radioBand == mCurrentRadioBand && mChannelLoader != null) { |
| mChannelLoader.forceLoad(); |
| } |
| } |
| |
| @Override |
| public Loader<List<RadioStation>> onCreateLoader(int id, Bundle args) { |
| // Only one loader, so no need to check for id. |
| mChannelLoader = new PreScannedChannelLoader(mActivity /* context */); |
| mChannelLoader.setCurrentRadioBand(mCurrentRadioBand); |
| |
| return mChannelLoader; |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<List<RadioStation>> loader, |
| List<RadioStation> preScannedStations) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| int size = preScannedStations == null ? 0 : preScannedStations.size(); |
| Log.d(TAG, "onLoadFinished(); number of pre-scanned stations: " + size); |
| } |
| |
| if (Log.isLoggable(TAG, Log.VERBOSE) && preScannedStations != null) { |
| for (RadioStation station : preScannedStations) { |
| Log.v(TAG, "station: " + station.toString()); |
| } |
| } |
| |
| mAdapter.setStations(preScannedStations); |
| |
| int position = mAdapter.setStartingStation(mCurrentChannelNumber, mCurrentRadioBand); |
| mRadioDisplayController.setCurrentStationInList(position); |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<List<RadioStation>> loader) {} |
| |
| /** |
| * Value animator for AM values. |
| */ |
| private ValueAnimator.AnimatorUpdateListener mAmAnimatorListener = |
| new ValueAnimator.AnimatorUpdateListener() { |
| public void onAnimationUpdate(ValueAnimator animation) { |
| mRadioDisplayController.setChannelNumber( |
| RadioChannelFormatter.AM_FORMATTER.format( |
| animation.getAnimatedValue())); |
| } |
| }; |
| |
| /** |
| * Value animator for FM values. |
| */ |
| private ValueAnimator.AnimatorUpdateListener mFmAnimatorListener = |
| new ValueAnimator.AnimatorUpdateListener() { |
| public void onAnimationUpdate(ValueAnimator animation) { |
| mRadioDisplayController.setChannelNumber( |
| RadioChannelFormatter.FM_FORMATTER.format( |
| animation.getAnimatedValue())); |
| } |
| }; |
| |
| private final IRadioCallback.Stub mCallback = new IRadioCallback.Stub() { |
| @Override |
| public void onRadioStationChanged(RadioStation station) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "onRadioStationChanged: " + station); |
| } |
| |
| if (station == null) { |
| return; |
| } |
| |
| if (mCurrentChannelNumber != station.getChannelNumber()) { |
| setRadioChannel(station.getChannelNumber()); |
| } |
| |
| onRadioMetadataChanged(station.getRds()); |
| |
| // Notify that the current radio station has changed. |
| if (mStationChangeListener != null) { |
| try { |
| mStationChangeListener.onRadioStationChanged( |
| mRadioManager.getCurrentRadioStation()); |
| } catch (RemoteException e) { |
| Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage()); |
| } |
| } |
| } |
| |
| /** |
| * Updates radio information based on the given {@link RadioRds}. |
| */ |
| @Override |
| public void onRadioMetadataChanged(RadioRds radioRds) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "onMetadataChanged(); metadata: " + radioRds); |
| } |
| |
| clearMetadataDisplay(); |
| |
| if (radioRds == null) { |
| return; |
| } |
| |
| mCurrentRds = radioRds; |
| |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "mCurrentRds: " + mCurrentRds); |
| } |
| |
| String programService = radioRds.getProgramService(); |
| String artistMetadata = radioRds.getSongArtist(); |
| |
| mRadioDisplayController.setCurrentSongArtistOrStation( |
| TextUtils.isEmpty(artistMetadata) ? programService : artistMetadata); |
| mRadioDisplayController.setCurrentSongTitle(radioRds.getSongTitle()); |
| |
| // Since new metadata exists, update the preset that is stored in the database if |
| // it exists. |
| if (TextUtils.isEmpty(programService)) { |
| return; |
| } |
| |
| RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */, |
| mCurrentRadioBand, radioRds); |
| boolean isPreset = mRadioStorage.isPreset(station); |
| |
| if (isPreset) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Current channel is a preset; updating metadata in the database."); |
| } |
| |
| mRadioStorage.storePreset(station); |
| } |
| } |
| |
| @Override |
| public void onRadioBandChanged(int radioBand) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "onRadioBandChanged: " + radioBand); |
| } |
| |
| setCurrentRadioBand(radioBand); |
| updateAmFmDisplayState(); |
| |
| // Check that the radio channel is being correctly formatted. |
| setRadioChannel(mCurrentChannelNumber); |
| } |
| |
| @Override |
| public void onRadioMuteChanged(boolean isMuted) { |
| mRadioDisplayController.setPlayPauseButtonState(isMuted); |
| } |
| |
| @Override |
| public void onError(int status) { |
| Log.e(TAG, "Radio callback error with status: " + status); |
| close(); |
| } |
| }; |
| |
| private final View.OnClickListener mBackwardSeekClickListener = new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mRadioManager == null) { |
| return; |
| } |
| |
| clearMetadataDisplay(); |
| |
| if (!mHasDualTuners) { |
| try { |
| mRadioManager.seekBackward(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "backwardSeek(); remote exception: " + e.getMessage()); |
| } |
| return; |
| } |
| |
| RadioStation prevStation = mAdapter.getPrevStation(); |
| |
| if (prevStation != null) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Seek backwards to station: " + prevStation); |
| } |
| |
| // Tune to the previous station, and then update the UI to reflect that tune. |
| try { |
| mRadioManager.tune(prevStation); |
| } catch (RemoteException e) { |
| Log.e(TAG, "backwardSeek(); remote exception: " + e.getMessage()); |
| } |
| |
| int position = mAdapter.getCurrentPosition(); |
| mRadioDisplayController.setCurrentStationInList(position); |
| } |
| } |
| }; |
| |
| private final View.OnClickListener mForwardSeekClickListener = new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mRadioManager == null) { |
| return; |
| } |
| |
| clearMetadataDisplay(); |
| |
| if (!mHasDualTuners) { |
| try { |
| mRadioManager.seekForward(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "forwardSeek(); remote exception: " + e.getMessage()); |
| } |
| return; |
| } |
| |
| RadioStation nextStation = mAdapter.getNextStation(); |
| |
| if (nextStation != null) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Seek forward to station: " + nextStation); |
| } |
| |
| // Tune to the next station, and then update the UI to reflect that tune. |
| try { |
| mRadioManager.tune(nextStation); |
| } catch (RemoteException e) { |
| Log.e(TAG, "forwardSeek(); remote exception: " + e.getMessage()); |
| } |
| |
| int position = mAdapter.getCurrentPosition(); |
| mRadioDisplayController.setCurrentStationInList(position); |
| } |
| } |
| }; |
| |
| /** |
| * Click listener for the play/pause button. Currently, all this does is mute/unmute the radio |
| * because the {@link RadioManager} does not support the ability to pause/start again. |
| */ |
| private final View.OnClickListener mPlayPauseClickListener = new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mRadioManager == null) { |
| return; |
| } |
| |
| try { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Play button clicked. Currently muted: " + mRadioManager.isMuted()); |
| } |
| |
| if (mRadioManager.isMuted()) { |
| mRadioManager.unMute(); |
| } else { |
| mRadioManager.mute(); |
| } |
| |
| boolean isMuted = mRadioManager.isMuted(); |
| |
| mUserHasMuted = isMuted; |
| mRadioDisplayController.setPlayPauseButtonState(isMuted); |
| } catch (RemoteException e) { |
| Log.e(TAG, "playPauseClickListener(); remote exception: " + e.getMessage()); |
| } |
| } |
| }; |
| |
| private final View.OnClickListener mPresetButtonClickListener = new View.OnClickListener() { |
| // TODO: Maybe add a check to send a store/remove preset event after a delay so that |
| // there aren't multiple writes if the user presses the button quickly. |
| @Override |
| public void onClick(View v) { |
| if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Attempting to store invalid radio station as a preset. Ignoring"); |
| } |
| |
| return; |
| } |
| |
| RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */, |
| mCurrentRadioBand, mCurrentRds); |
| boolean isPreset = mRadioStorage.isPreset(station); |
| |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Toggling preset for " + station |
| + "\n\tIs currently a preset: " + isPreset); |
| } |
| |
| if (isPreset) { |
| mRadioStorage.removePreset(station); |
| } else { |
| mRadioStorage.storePreset(station); |
| } |
| |
| // Update the UI immediately. If the preset failed for some reason, the RadioStorage |
| // will notify us and UI update will happen then. |
| mRadioDisplayController.setChannelIsPreset(!isPreset); |
| } |
| }; |
| |
| private ServiceConnection mServiceConnection = new ServiceConnection() { |
| @Override |
| public void onServiceConnected(ComponentName className, IBinder binder) { |
| mRadioManager = ((IRadioManager) binder); |
| |
| try { |
| if (mRadioManager == null || !mRadioManager.isInitialized()) { |
| mRadioDisplayController.setEnabled(false); |
| |
| if (mRadioErrorDisplay != null) { |
| mRadioErrorDisplay.setVisibility(View.VISIBLE); |
| } |
| |
| return; |
| } |
| |
| mRadioDisplayController.setEnabled(true); |
| |
| if (mRadioErrorDisplay != null) { |
| mRadioErrorDisplay.setVisibility(View.GONE); |
| } |
| |
| mHasDualTuners = mRadioManager.hasDualTuners(); |
| |
| if (mHasDualTuners) { |
| initializeDualTunerController(); |
| } else { |
| mRadioDisplayController.setSingleChannelDisplay(mRadioBackground); |
| } |
| |
| mRadioManager.addRadioTunerCallback(mCallback); |
| |
| int radioBand = mRadioStorage.getStoredRadioBand(); |
| |
| // Upon successful connection, open the radio. |
| openRadioBand(radioBand); |
| maybeTuneToStoredRadioChannel(); |
| |
| if (mStationChangeListener != null) { |
| mStationChangeListener.onRadioStationChanged( |
| mRadioManager.getCurrentRadioStation()); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "onServiceConnected(); remote exception: " + e.getMessage()); |
| } |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName className) { |
| mRadioManager = null; |
| } |
| }; |
| |
| private final ValueAnimator.AnimatorUpdateListener mBackgroundColorUpdater = |
| animator -> { |
| int backgroundColor = (int) animator.getAnimatedValue(); |
| setBackgroundColor(backgroundColor); |
| }; |
| } |