blob: 14b131ef197e2e1f1e34e41b069fe5aa150b9bdd [file] [log] [blame]
/*
* 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);
}
}