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