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