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.ValueAnimator; 20 import android.content.Context; 21 import android.hardware.radio.ProgramSelector; 22 import android.media.session.PlaybackState; 23 import android.text.TextUtils; 24 import android.view.View; 25 import android.widget.ImageView; 26 import android.widget.TextView; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 import androidx.fragment.app.FragmentActivity; 31 32 import com.android.car.broadcastradio.support.platform.ProgramSelectorExt; 33 import com.android.car.radio.bands.ProgramType; 34 import com.android.car.radio.service.RadioAppServiceWrapper; 35 import com.android.car.radio.service.RadioAppServiceWrapper.ConnectionState; 36 import com.android.car.radio.util.Log; 37 import com.android.car.radio.widget.PlayPauseButton; 38 39 import java.util.Objects; 40 41 /** 42 * Controller that controls the appearance state of various UI elements in the radio. 43 */ 44 public class DisplayController { 45 private static final String TAG = "BcRadioApp.display"; 46 47 private static final int CHANNEL_CHANGE_DURATION_MS = 200; 48 private static final char EN_DASH = '\u2013'; 49 private static final String DETAILS_SEPARATOR = " " + EN_DASH + " "; 50 51 private final Context mContext; 52 53 private final View mViewpager; 54 private final TextView mStatusMessage; 55 private final TextView mChannel; 56 private final TextView mDetails; 57 private final TextView mStationName; 58 59 private final ImageView mBackwardSeekButton; 60 private final ImageView mForwardSeekButton; 61 private final PlayPauseButton mPlayButton; 62 63 private boolean mIsFavorite = false; 64 private final ImageView mFavoriteButton; 65 private FavoriteToggleListener mFavoriteToggleListener; 66 67 private final ValueAnimator mChannelAnimator = new ValueAnimator(); 68 private @Nullable ProgramSelector mDisplayedChannel; 69 70 /** 71 * Callback for favorite toggle button. 72 */ 73 public interface FavoriteToggleListener { 74 /** 75 * Called when favorite toggle button was clicked. 76 * 77 * @param addFavorite {@code} true, if the callback should add the current program to 78 * favorites, {@code false} otherwise. 79 */ onFavoriteToggled(boolean addFavorite)80 void onFavoriteToggled(boolean addFavorite); 81 } 82 DisplayController(@onNull FragmentActivity activity, @NonNull RadioController radioController)83 public DisplayController(@NonNull FragmentActivity activity, 84 @NonNull RadioController radioController) { 85 mContext = Objects.requireNonNull(activity); 86 87 mViewpager = activity.findViewById(R.id.viewpager); 88 mStatusMessage = activity.findViewById(R.id.status_message); 89 mChannel = activity.findViewById(R.id.station_channel); 90 mDetails = activity.findViewById(R.id.station_details); 91 mStationName = activity.findViewById(R.id.station_name); 92 mBackwardSeekButton = activity.findViewById(R.id.back_button); 93 mForwardSeekButton = activity.findViewById(R.id.forward_button); 94 mPlayButton = activity.findViewById(R.id.play_button); 95 mFavoriteButton = activity.findViewById(R.id.add_presets_button); 96 97 radioController.getPlaybackState().observe(activity, this::onPlaybackStateChanged); 98 99 if (mFavoriteButton != null) { 100 mFavoriteButton.setOnClickListener(v -> { 101 FavoriteToggleListener listener = mFavoriteToggleListener; 102 if (listener != null) listener.onFavoriteToggled(!mIsFavorite); 103 }); 104 } 105 } 106 107 /** 108 * Sets application state. 109 * 110 * This shows/hides the UI elements and may display error messages (depending on the current 111 * application state). 112 * 113 * If the UI is disabled/hidden, then no callbacks will be triggered. 114 * 115 * @param state Current application state 116 */ setState(@onnectionState int state)117 public void setState(@ConnectionState int state) { 118 Log.d(TAG, "Adjusting the UI to a new application state: " + state); 119 120 boolean enabled = (state == RadioAppServiceWrapper.STATE_CONNECTED); 121 122 // Color the buttons so that they are grey in appearance if they are disabled. 123 int tint = enabled 124 ? mContext.getColor(R.color.control_button_color) 125 : mContext.getColor(R.color.control_button_disabled_color); 126 127 if (mPlayButton != null) { 128 // No need to tint the play button because its drawable already contains a disabled 129 // state. 130 mPlayButton.setEnabled(enabled); 131 } 132 133 if (mForwardSeekButton != null) { 134 mForwardSeekButton.setEnabled(enabled); 135 mForwardSeekButton.setColorFilter(tint); 136 } 137 if (mBackwardSeekButton != null) { 138 mBackwardSeekButton.setEnabled(enabled); 139 mBackwardSeekButton.setColorFilter(tint); 140 } 141 142 if (mFavoriteButton != null) { 143 mFavoriteButton.setVisibility(enabled ? View.VISIBLE : View.GONE); 144 } 145 if (mViewpager != null) { 146 mViewpager.setVisibility(enabled ? View.VISIBLE : View.GONE); 147 } 148 149 if (mStatusMessage != null) { 150 mStatusMessage.setVisibility(enabled ? View.GONE : View.VISIBLE); 151 switch (state) { 152 case RadioAppServiceWrapper.STATE_CONNECTING: 153 case RadioAppServiceWrapper.STATE_CONNECTED: 154 mStatusMessage.setText(null); 155 break; 156 case RadioAppServiceWrapper.STATE_NOT_SUPPORTED: 157 mStatusMessage.setText(R.string.radio_not_supported_text); 158 break; 159 case RadioAppServiceWrapper.STATE_ERROR: 160 mStatusMessage.setText(R.string.radio_failure_text); 161 break; 162 } 163 } 164 } 165 166 /** 167 * Sets the {@link android.view.View.OnClickListener} for the backwards seek button. 168 */ setBackwardSeekButtonListener(View.OnClickListener listener)169 public void setBackwardSeekButtonListener(View.OnClickListener listener) { 170 if (mBackwardSeekButton != null) { 171 mBackwardSeekButton.setOnClickListener(listener); 172 } 173 } 174 175 /** 176 * Sets the {@link android.view.View.OnClickListener} for the forward seek button. 177 */ setForwardSeekButtonListener(View.OnClickListener listener)178 public void setForwardSeekButtonListener(View.OnClickListener listener) { 179 if (mForwardSeekButton != null) { 180 mForwardSeekButton.setOnClickListener(listener); 181 } 182 } 183 184 /** 185 * Sets the callback for the play/pause button. 186 */ setPlayButtonCallback(@ullable PlayPauseButton.Callback callback)187 public void setPlayButtonCallback(@Nullable PlayPauseButton.Callback callback) { 188 if (mPlayButton == null) return; 189 mPlayButton.setCallback(callback); 190 } 191 192 /** 193 * Sets the listener for favorite toggle button. 194 * 195 * @param listener Listener to set, or {@code null} to remove 196 */ setFavoriteToggleListener(@ullable FavoriteToggleListener listener)197 public void setFavoriteToggleListener(@Nullable FavoriteToggleListener listener) { 198 mFavoriteToggleListener = listener; 199 } 200 201 /** 202 * Sets the current radio channel (e.g. 88.5 FM). 203 * 204 * If the channel is of the same type (band) as currently displayed, animates the transition. 205 * 206 * @param sel Channel to display 207 */ setChannel(@ullable ProgramSelector sel)208 public void setChannel(@Nullable ProgramSelector sel) { 209 if (mChannel == null) return; 210 211 mChannelAnimator.cancel(); 212 213 if (sel == null) { 214 mChannel.setText(null); 215 } else if (!ProgramSelectorExt.isAmFmProgram(sel) 216 || !ProgramSelectorExt.hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) { 217 // channel animation is implemented for AM/FM only 218 mChannel.setText(ProgramSelectorExt.getDisplayName(sel, 0)); 219 } else if (ProgramType.fromSelector(mDisplayedChannel) 220 != ProgramType.fromSelector(sel)) { 221 // it's a different band - don't animate 222 mChannel.setText(ProgramSelectorExt.getDisplayName(sel, 0)); 223 } else { 224 int fromFreq = (int) mDisplayedChannel 225 .getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY); 226 int toFreq = (int) sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY); 227 mChannelAnimator.setIntValues((int) fromFreq, (int) toFreq); 228 mChannelAnimator.setDuration(CHANNEL_CHANGE_DURATION_MS); 229 mChannelAnimator.addUpdateListener(animation -> mChannel.setText( 230 ProgramSelectorExt.formatAmFmFrequency((int) animation.getAnimatedValue(), 0))); 231 mChannelAnimator.start(); 232 } 233 234 mDisplayedChannel = sel; 235 } 236 237 /** 238 * Sets program details. 239 * 240 * @param details Program details (title/artist or radio text). 241 */ setDetails(@ullable String details)242 public void setDetails(@Nullable String details) { 243 if (mDetails == null) return; 244 mDetails.setText(details); 245 mDetails.setVisibility(TextUtils.isEmpty(details) ? View.INVISIBLE : View.VISIBLE); 246 } 247 248 /** 249 * Sets program details (title/artist of currently playing song). 250 * 251 * @param songTitle Title of currently playing song 252 * @param songArtist Artist of currently playing song 253 */ setDetails(@ullable String songTitle, @Nullable String songArtist)254 public void setDetails(@Nullable String songTitle, @Nullable String songArtist) { 255 if (mDetails == null) return; 256 songTitle = songTitle.trim(); 257 songArtist = songArtist.trim(); 258 if (TextUtils.isEmpty(songTitle)) songTitle = null; 259 if (TextUtils.isEmpty(songArtist)) songArtist = null; 260 261 String details; 262 if (songTitle == null && songArtist == null) { 263 details = null; 264 } else if (songTitle == null) { 265 details = songArtist; 266 } else if (songArtist == null) { 267 details = songTitle; 268 } else { 269 details = songArtist + DETAILS_SEPARATOR + songTitle; 270 } 271 272 setDetails(details); 273 } 274 275 /** 276 * Sets the artist(s) of the currently playing song or current radio station information 277 * (e.g. KOIT). 278 */ setStationName(@ullable String name)279 public void setStationName(@Nullable String name) { 280 if (mStationName == null) return; 281 boolean isEmpty = TextUtils.isEmpty(name); 282 mStationName.setText(isEmpty ? null : name.trim()); 283 mStationName.setVisibility(isEmpty ? View.INVISIBLE : View.VISIBLE); 284 } 285 onPlaybackStateChanged(@laybackState.State int state)286 private void onPlaybackStateChanged(@PlaybackState.State int state) { 287 if (mPlayButton != null) { 288 mPlayButton.setPlayState(state); 289 mPlayButton.refreshDrawableState(); 290 } 291 } 292 293 /** 294 * Sets whether or not the current program is stored as a favorite. If it is, then the 295 * icon will be updatd to reflect this state. 296 */ setCurrentIsFavorite(boolean isFavorite)297 public void setCurrentIsFavorite(boolean isFavorite) { 298 mIsFavorite = isFavorite; 299 if (mFavoriteButton == null) return; 300 mFavoriteButton.setImageResource( 301 isFavorite ? R.drawable.ic_star_filled : R.drawable.ic_star_empty); 302 } 303 304 /** 305 * Starts seek animation. 306 * 307 * TODO(b/111340798): implement actual animation 308 * TODO(b/111340798): remove forward parameter, if not necessary for animation 309 * 310 * @param forward {@code true} for forward seek, {@code false} otherwise. 311 */ startSeekAnimation(boolean forward)312 public void startSeekAnimation(boolean forward) { 313 // TODO(b/111340798): watch for timeout and if it happens, display metadata back 314 setStationName(null); 315 setDetails(null); 316 } 317 } 318