| /* |
| * 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.ValueAnimator; |
| import android.content.Context; |
| import android.hardware.radio.ProgramSelector; |
| import android.media.session.PlaybackState; |
| import android.text.TextUtils; |
| import android.view.View; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.fragment.app.FragmentActivity; |
| |
| import com.android.car.broadcastradio.support.platform.ProgramSelectorExt; |
| import com.android.car.radio.bands.ProgramType; |
| import com.android.car.radio.service.RadioAppServiceWrapper; |
| import com.android.car.radio.service.RadioAppServiceWrapper.ConnectionState; |
| import com.android.car.radio.util.Log; |
| import com.android.car.radio.widget.PlayPauseButton; |
| |
| import java.util.Objects; |
| |
| /** |
| * Controller that controls the appearance state of various UI elements in the radio. |
| */ |
| public class DisplayController { |
| private static final String TAG = "BcRadioApp.display"; |
| |
| private static final int CHANNEL_CHANGE_DURATION_MS = 200; |
| private static final char EN_DASH = '\u2013'; |
| private static final String DETAILS_SEPARATOR = " " + EN_DASH + " "; |
| |
| private final Context mContext; |
| |
| private final View mToolbar; |
| private final View mViewpager; |
| private final TextView mStatusMessage; |
| private final TextView mChannel; |
| private final TextView mDetails; |
| private final TextView mStationName; |
| |
| private final ImageView mBackwardSeekButton; |
| private final ImageView mForwardSeekButton; |
| private final PlayPauseButton mPlayButton; |
| |
| private boolean mIsFavorite = false; |
| private final ImageView mFavoriteButton; |
| private FavoriteToggleListener mFavoriteToggleListener; |
| |
| private final ValueAnimator mChannelAnimator = new ValueAnimator(); |
| private @Nullable ProgramSelector mDisplayedChannel; |
| |
| /** |
| * Callback for favorite toggle button. |
| */ |
| public interface FavoriteToggleListener { |
| /** |
| * Called when favorite toggle button was clicked. |
| * |
| * @param addFavorite {@code} true, if the callback should add the current program to |
| * favorites, {@code false} otherwise. |
| */ |
| void onFavoriteToggled(boolean addFavorite); |
| } |
| |
| public DisplayController(@NonNull FragmentActivity activity, |
| @NonNull RadioController radioController) { |
| mContext = Objects.requireNonNull(activity); |
| |
| mToolbar = activity.findViewById(R.id.toolbar); |
| mViewpager = activity.findViewById(R.id.viewpager); |
| mStatusMessage = activity.findViewById(R.id.status_message); |
| mChannel = activity.findViewById(R.id.station_channel); |
| mDetails = activity.findViewById(R.id.station_details); |
| mStationName = activity.findViewById(R.id.station_name); |
| mBackwardSeekButton = activity.findViewById(R.id.back_button); |
| mForwardSeekButton = activity.findViewById(R.id.forward_button); |
| mPlayButton = activity.findViewById(R.id.play_button); |
| mFavoriteButton = activity.findViewById(R.id.add_presets_button); |
| |
| radioController.getPlaybackState().observe(activity, this::onPlaybackStateChanged); |
| |
| if (mFavoriteButton != null) { |
| mFavoriteButton.setOnClickListener(v -> { |
| FavoriteToggleListener listener = mFavoriteToggleListener; |
| if (listener != null) listener.onFavoriteToggled(!mIsFavorite); |
| }); |
| } |
| } |
| |
| /** |
| * Sets application state. |
| * |
| * This shows/hides the UI elements and may display error messages (depending on the current |
| * application state). |
| * |
| * If the UI is disabled/hidden, then no callbacks will be triggered. |
| * |
| * @param state Current application state |
| */ |
| public void setState(@ConnectionState int state) { |
| Log.d(TAG, "Adjusting the UI to a new application state: " + state); |
| |
| boolean enabled = (state == RadioAppServiceWrapper.STATE_CONNECTED); |
| |
| // Color the buttons so that they are grey in appearance if they are disabled. |
| int tint = enabled |
| ? mContext.getColor(R.color.control_button_color) |
| : mContext.getColor(R.color.control_button_disabled_color); |
| |
| if (mPlayButton != null) { |
| // No need to tint the play button because its drawable already contains a disabled |
| // state. |
| mPlayButton.setEnabled(enabled); |
| } |
| |
| if (mForwardSeekButton != null) { |
| mForwardSeekButton.setEnabled(enabled); |
| mForwardSeekButton.setColorFilter(tint); |
| } |
| if (mBackwardSeekButton != null) { |
| mBackwardSeekButton.setEnabled(enabled); |
| mBackwardSeekButton.setColorFilter(tint); |
| } |
| |
| if (mFavoriteButton != null) { |
| mFavoriteButton.setVisibility(enabled ? View.VISIBLE : View.GONE); |
| } |
| if (mToolbar != null) { |
| mToolbar.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); |
| } |
| if (mViewpager != null) { |
| mViewpager.setVisibility(enabled ? View.VISIBLE : View.GONE); |
| } |
| |
| if (mStatusMessage != null) { |
| mStatusMessage.setVisibility(enabled ? View.GONE : View.VISIBLE); |
| switch (state) { |
| case RadioAppServiceWrapper.STATE_CONNECTING: |
| case RadioAppServiceWrapper.STATE_CONNECTED: |
| mStatusMessage.setText(null); |
| break; |
| case RadioAppServiceWrapper.STATE_NOT_SUPPORTED: |
| mStatusMessage.setText(R.string.radio_not_supported_text); |
| break; |
| case RadioAppServiceWrapper.STATE_ERROR: |
| mStatusMessage.setText(R.string.radio_failure_text); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Sets the {@link android.view.View.OnClickListener} for the backwards seek button. |
| */ |
| public void setBackwardSeekButtonListener(View.OnClickListener listener) { |
| if (mBackwardSeekButton != null) { |
| mBackwardSeekButton.setOnClickListener(listener); |
| } |
| } |
| |
| /** |
| * Sets the {@link android.view.View.OnClickListener} for the forward seek button. |
| */ |
| public void setForwardSeekButtonListener(View.OnClickListener listener) { |
| if (mForwardSeekButton != null) { |
| mForwardSeekButton.setOnClickListener(listener); |
| } |
| } |
| |
| /** |
| * Sets the callback for the play/pause button. |
| */ |
| public void setPlayButtonCallback(@Nullable PlayPauseButton.Callback callback) { |
| if (mPlayButton == null) return; |
| mPlayButton.setCallback(callback); |
| } |
| |
| /** |
| * Sets the listener for favorite toggle button. |
| * |
| * @param listener Listener to set, or {@code null} to remove |
| */ |
| public void setFavoriteToggleListener(@Nullable FavoriteToggleListener listener) { |
| mFavoriteToggleListener = listener; |
| } |
| |
| /** |
| * Sets the current radio channel (e.g. 88.5 FM). |
| * |
| * If the channel is of the same type (band) as currently displayed, animates the transition. |
| * |
| * @param sel Channel to display |
| */ |
| public void setChannel(@Nullable ProgramSelector sel) { |
| if (mChannel == null) return; |
| |
| mChannelAnimator.cancel(); |
| |
| if (sel == null) { |
| mChannel.setText(null); |
| } else if (!ProgramSelectorExt.isAmFmProgram(sel) |
| || !ProgramSelectorExt.hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) { |
| // channel animation is implemented for AM/FM only |
| mChannel.setText(ProgramSelectorExt.getDisplayName(sel, 0)); |
| } else if (ProgramType.fromSelector(mDisplayedChannel) |
| != ProgramType.fromSelector(sel)) { |
| // it's a different band - don't animate |
| mChannel.setText(ProgramSelectorExt.getDisplayName(sel, 0)); |
| } else { |
| int fromFreq = (int) mDisplayedChannel |
| .getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY); |
| int toFreq = (int) sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY); |
| mChannelAnimator.setIntValues((int) fromFreq, (int) toFreq); |
| mChannelAnimator.setDuration(CHANNEL_CHANGE_DURATION_MS); |
| mChannelAnimator.addUpdateListener(animation -> mChannel.setText( |
| ProgramSelectorExt.formatAmFmFrequency((int) animation.getAnimatedValue(), 0))); |
| mChannelAnimator.start(); |
| } |
| |
| mDisplayedChannel = sel; |
| } |
| |
| /** |
| * Sets program details. |
| * |
| * @param details Program details (title/artist or radio text). |
| */ |
| public void setDetails(@Nullable String details) { |
| if (mDetails == null) return; |
| mDetails.setText(details); |
| mDetails.setVisibility(TextUtils.isEmpty(details) ? View.INVISIBLE : View.VISIBLE); |
| } |
| |
| /** |
| * Sets program details (title/artist of currently playing song). |
| * |
| * @param songTitle Title of currently playing song |
| * @param songArtist Artist of currently playing song |
| */ |
| public void setDetails(@Nullable String songTitle, @Nullable String songArtist) { |
| if (mDetails == null) return; |
| songTitle = songTitle.trim(); |
| songArtist = songArtist.trim(); |
| if (TextUtils.isEmpty(songTitle)) songTitle = null; |
| if (TextUtils.isEmpty(songArtist)) songArtist = null; |
| |
| String details; |
| if (songTitle == null && songArtist == null) { |
| details = null; |
| } else if (songTitle == null) { |
| details = songArtist; |
| } else if (songArtist == null) { |
| details = songTitle; |
| } else { |
| details = songArtist + DETAILS_SEPARATOR + songTitle; |
| } |
| |
| setDetails(details); |
| } |
| |
| /** |
| * Sets the artist(s) of the currently playing song or current radio station information |
| * (e.g. KOIT). |
| */ |
| public void setStationName(@Nullable String name) { |
| if (mStationName == null) return; |
| boolean isEmpty = TextUtils.isEmpty(name); |
| mStationName.setText(isEmpty ? null : name.trim()); |
| mStationName.setVisibility(isEmpty ? View.INVISIBLE : View.VISIBLE); |
| } |
| |
| private void onPlaybackStateChanged(@PlaybackState.State int state) { |
| if (mPlayButton != null) { |
| mPlayButton.setPlayState(state); |
| mPlayButton.refreshDrawableState(); |
| } |
| } |
| |
| /** |
| * Sets whether or not the current program is stored as a favorite. If it is, then the |
| * icon will be updatd to reflect this state. |
| */ |
| public void setCurrentIsFavorite(boolean isFavorite) { |
| mIsFavorite = isFavorite; |
| if (mFavoriteButton == null) return; |
| mFavoriteButton.setImageResource( |
| isFavorite ? R.drawable.ic_star_filled : R.drawable.ic_star_empty); |
| } |
| |
| /** |
| * Starts seek animation. |
| * |
| * TODO(b/111340798): implement actual animation |
| * TODO(b/111340798): remove forward parameter, if not necessary for animation |
| * |
| * @param forward {@code true} for forward seek, {@code false} otherwise. |
| */ |
| public void startSeekAnimation(boolean forward) { |
| // TODO(b/111340798): watch for timeout and if it happens, display metadata back |
| setStationName(null); |
| setDetails(null); |
| } |
| } |