• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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