1 /* 2 * Copyright 2019 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.media.common; 18 19 import android.content.Context; 20 import android.content.res.ColorStateList; 21 import android.graphics.PorterDuff; 22 import android.graphics.drawable.Drawable; 23 import android.graphics.drawable.VectorDrawable; 24 import android.util.Log; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.widget.ImageButton; 28 import android.widget.ProgressBar; 29 30 import androidx.annotation.ColorRes; 31 import androidx.annotation.DrawableRes; 32 import androidx.annotation.LayoutRes; 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.lifecycle.LifecycleOwner; 36 37 import com.android.car.apps.common.CarControlBar; 38 import com.android.car.apps.common.CommonFlags; 39 import com.android.car.apps.common.ControlBar; 40 import com.android.car.media.common.playback.PlaybackViewModel; 41 import com.android.car.media.common.source.MediaSourceColors; 42 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.stream.Collectors; 48 49 /** 50 * This class manages the media control buttons that are added to a CarControlBar. 51 * 52 * It expects the icons to be {@link VectorDrawable}s (because they look better), and will flag 53 * non compliant ones if {@link MediaItemMetadata#flagInvalidMediaArt} returns true. 54 */ 55 public class MediaButtonController { 56 57 private static final String TAG = "MediaButton"; 58 59 private final Map<String, ImageButton> mImageButtons = new HashMap<>(); 60 61 private Context mContext; 62 private PlayPauseStopImageView mPlayPauseStopImageView; 63 private View mPlayPauseStopImageContainer; 64 private ProgressBar mCircularProgressBar; 65 private ImageButton mSkipPrevButton; 66 private ImageButton mSkipNextButton; 67 private ColorStateList mIconsColor; 68 private boolean mSkipNextAdded; 69 private boolean mSkipPrevAdded; 70 private boolean mShowCircularProgressBar; 71 72 private PlaybackViewModel mModel; 73 private PlaybackViewModel.PlaybackController mController; 74 75 private CarControlBar mControlBar; 76 MediaButtonController(Context context, CarControlBar controlBar, @ColorRes int iconColorsId, @LayoutRes int playPauseContainerId, @DrawableRes int skipPrevButtonId, @DrawableRes int skipNextButtonId)77 public MediaButtonController(Context context, CarControlBar controlBar, 78 @ColorRes int iconColorsId, @LayoutRes int playPauseContainerId, 79 @DrawableRes int skipPrevButtonId, @DrawableRes int skipNextButtonId) { 80 mContext = context; 81 mControlBar = controlBar; 82 mPlayPauseStopImageContainer = 83 LayoutInflater.from(context).inflate(playPauseContainerId, null); 84 mPlayPauseStopImageContainer.setOnClickListener(this::onPlayPauseStopClicked); 85 mPlayPauseStopImageView = mPlayPauseStopImageContainer.findViewById(R.id.play_pause_stop); 86 mPlayPauseStopImageView.setVisibility(View.INVISIBLE); 87 mCircularProgressBar = mPlayPauseStopImageContainer.findViewById( 88 R.id.circular_progress_bar); 89 mPlayPauseStopImageView.setAction(PlayPauseStopImageView.ACTION_DISABLED); 90 mPlayPauseStopImageView.setOnClickListener(this::onPlayPauseStopClicked); 91 // In non-touch mode, a browse list will request focus explicitly and its first element 92 // will get focused instead of this button 93 mPlayPauseStopImageView.setFocusedByDefault(true); 94 95 mShowCircularProgressBar = context.getResources().getBoolean( 96 R.bool.show_circular_progress_bar); 97 mIconsColor = context.getResources().getColorStateList(iconColorsId, null); 98 99 mSkipPrevButton = createIconButton(context.getDrawable(skipPrevButtonId)); 100 mSkipPrevButton.setId(R.id.skip_prev); 101 mSkipPrevButton.setVisibility(View.VISIBLE); 102 mSkipPrevButton.setOnClickListener(this::onPrevClicked); 103 104 mSkipNextButton = createIconButton(context.getDrawable(skipNextButtonId)); 105 mSkipNextButton.setId(R.id.skip_next); 106 mSkipNextButton.setVisibility(View.VISIBLE); 107 mSkipNextButton.setOnClickListener(this::onNextClicked); 108 109 resetInitialViews(); 110 } 111 112 /** 113 * Set the model that the available media buttons will be provided from 114 */ setModel(@onNull PlaybackViewModel model, @NonNull LifecycleOwner owner)115 public void setModel(@NonNull PlaybackViewModel model, @NonNull LifecycleOwner owner) { 116 if (mModel != null) { 117 Log.w(TAG, "PlaybackViewModel set more than once. Ignoring subsequent call."); 118 } 119 mModel = model; 120 121 model.getPlaybackController().observe(owner, controller -> { 122 if (mController != controller) { 123 mController = controller; 124 resetInitialViews(); 125 } 126 }); 127 mPlayPauseStopImageView.setVisibility(View.VISIBLE); 128 boolean useMediaSourceColor = 129 mContext.getResources().getBoolean(R.bool.use_media_source_color_for_fab_spinner); 130 if (useMediaSourceColor) { 131 model.getMediaSourceColors().observe(owner, this::updateSpinerColors); 132 } 133 model.getPlaybackStateWrapper().observe(owner, this::onPlaybackStateChanged); 134 } 135 resetInitialViews()136 private void resetInitialViews() { 137 mControlBar.setViews(new View[0]); 138 mControlBar.setView(mPlayPauseStopImageContainer, ControlBar.SLOT_MAIN); 139 mControlBar.setView(null, ControlBar.SLOT_LEFT); 140 mControlBar.setView(null, ControlBar.SLOT_RIGHT); 141 mSkipNextAdded = false; 142 mSkipPrevAdded = false; 143 mImageButtons.clear(); 144 } 145 createIconButton(Drawable icon)146 private ImageButton createIconButton(Drawable icon) { 147 ImageButton button = mControlBar.createIconButton(icon); 148 boolean flagInvalidArt = CommonFlags.getInstance(mContext).shouldFlagImproperImageRefs(); 149 if (flagInvalidArt && !(icon instanceof VectorDrawable)) { 150 button.setImageTintList( 151 ColorStateList.valueOf(MediaItemMetadata.INVALID_MEDIA_ART_TINT_COLOR)); 152 } else { 153 button.setImageTintList(mIconsColor); 154 } 155 button.setImageTintMode(PorterDuff.Mode.SRC_ATOP); 156 return button; 157 } 158 onPlaybackStateChanged(@ullable PlaybackViewModel.PlaybackStateWrapper state)159 private void onPlaybackStateChanged(@Nullable PlaybackViewModel.PlaybackStateWrapper state) { 160 161 boolean hasState = (state != null); 162 mPlayPauseStopImageView.setAction(convertMainAction(state)); 163 boolean isLoading = hasState && state.isLoading(); 164 mCircularProgressBar.setVisibility( 165 isLoading || mShowCircularProgressBar ? View.VISIBLE : View.INVISIBLE); 166 mCircularProgressBar.setIndeterminate(isLoading); 167 168 // If prev/next is reserved, but not enabled, the icon is displayed as disabled (inactive 169 // or grayed out). For example some apps only allow a certain number of skips in a given 170 // time. 171 172 boolean skipPreviousReserved = hasState && state.iSkipPreviousReserved(); 173 boolean skipPreviousEnabled = hasState && state.isSkipPreviousEnabled(); 174 175 if (skipPreviousReserved || skipPreviousEnabled) { 176 if (!mSkipPrevAdded) { 177 mControlBar.setView(mSkipPrevButton, ControlBar.SLOT_LEFT); 178 mSkipPrevAdded = true; 179 } 180 } else { 181 mControlBar.setView(null, ControlBar.SLOT_LEFT); 182 mSkipPrevAdded = false; 183 } 184 mSkipPrevButton.setEnabled(skipPreviousEnabled); 185 186 boolean skipNextReserved = hasState && state.isSkipNextReserved(); 187 boolean skipNextEnabled = hasState && state.isSkipNextEnabled(); 188 189 if (skipNextReserved || skipNextEnabled) { 190 if (!mSkipNextAdded) { 191 mControlBar.setView(mSkipNextButton, ControlBar.SLOT_RIGHT); 192 mSkipNextAdded = true; 193 } 194 } else { 195 mControlBar.setView(null, ControlBar.SLOT_RIGHT); 196 mSkipNextAdded = false; 197 } 198 mSkipNextButton.setEnabled(skipNextEnabled); 199 200 updateCustomActions(state); 201 } 202 203 @PlayPauseStopImageView.Action convertMainAction(@ullable PlaybackViewModel.PlaybackStateWrapper state)204 private int convertMainAction(@Nullable PlaybackViewModel.PlaybackStateWrapper state) { 205 @PlaybackViewModel.Action int action = 206 (state != null) ? state.getMainAction() : PlaybackViewModel.ACTION_DISABLED; 207 switch (action) { 208 case PlaybackViewModel.ACTION_DISABLED: 209 return PlayPauseStopImageView.ACTION_DISABLED; 210 case PlaybackViewModel.ACTION_PLAY: 211 return PlayPauseStopImageView.ACTION_PLAY; 212 case PlaybackViewModel.ACTION_PAUSE: 213 return PlayPauseStopImageView.ACTION_PAUSE; 214 case PlaybackViewModel.ACTION_STOP: 215 return PlayPauseStopImageView.ACTION_STOP; 216 } 217 Log.w(TAG, "Unknown action: " + action); 218 return PlayPauseStopImageView.ACTION_DISABLED; 219 } 220 updateSpinerColors(MediaSourceColors colors)221 private void updateSpinerColors(MediaSourceColors colors) { 222 int color = getMediaSourceColor(colors); 223 mCircularProgressBar.setIndeterminateTintList(ColorStateList.valueOf(color)); 224 } 225 getMediaSourceColor(@ullable MediaSourceColors colors)226 private int getMediaSourceColor(@Nullable MediaSourceColors colors) { 227 int defaultColor = mContext.getResources().getColor(R.color.media_source_default_color, 228 null); 229 return colors != null ? colors.getAccentColor(defaultColor) : defaultColor; 230 } 231 updateCustomActions(@ullable PlaybackViewModel.PlaybackStateWrapper state)232 private void updateCustomActions(@Nullable PlaybackViewModel.PlaybackStateWrapper state) { 233 int focusedViewIndex = mControlBar.getFocusedViewIndex(); 234 235 List<ImageButton> imageButtons = new ArrayList<>(); 236 if (state != null) { 237 imageButtons.addAll(state.getCustomActions() 238 .stream() 239 .map(rawAction -> rawAction.fetchDrawable(mContext)) 240 .map(action -> getOrCreateIconButton(action)) 241 .collect(Collectors.toList())); 242 } 243 if (!mSkipPrevAdded && !imageButtons.isEmpty()) { 244 mControlBar.setView(imageButtons.remove(0), CarControlBar.SLOT_LEFT); 245 } 246 if (!mSkipNextAdded && !imageButtons.isEmpty()) { 247 mControlBar.setView(imageButtons.remove(0), CarControlBar.SLOT_RIGHT); 248 } 249 mControlBar.setViews(imageButtons.toArray(new ImageButton[0])); 250 251 mControlBar.setFocusAtViewIndex(focusedViewIndex); 252 } 253 getOrCreateIconButton(CustomPlaybackAction action)254 private ImageButton getOrCreateIconButton(CustomPlaybackAction action) { 255 // Reuse the ImageButton with the same action identifier if it exists, because if the 256 // ImageButton is focused, replacing it with a new one will make it lose focus. 257 ImageButton button = mImageButtons.get(action.mAction); 258 if (button != null) { 259 button.setImageDrawable(action.mIcon); 260 } else { 261 button = createIconButton(action.mIcon); 262 mImageButtons.put(action.mAction, button); 263 } 264 button.setOnClickListener(view -> 265 mController.doCustomAction(action.mAction, action.mExtras)); 266 return button; 267 } 268 onPlayPauseStopClicked(View view)269 private void onPlayPauseStopClicked(View view) { 270 if (mController == null) { 271 return; 272 } 273 switch (mPlayPauseStopImageView.getAction()) { 274 case PlayPauseStopImageView.ACTION_PLAY: 275 mController.play(); 276 break; 277 case PlayPauseStopImageView.ACTION_PAUSE: 278 mController.pause(); 279 break; 280 case PlayPauseStopImageView.ACTION_STOP: 281 mController.stop(); 282 break; 283 default: 284 Log.i(TAG, "Play/Pause/Stop clicked on invalid state"); 285 break; 286 } 287 } 288 onNextClicked(View view)289 private void onNextClicked(View view) { 290 PlaybackViewModel.PlaybackStateWrapper state = getPlaybackState(); 291 if ((mController != null) && (state != null) && (state.isSkipNextEnabled())) { 292 mController.skipToNext(); 293 } 294 } 295 onPrevClicked(View view)296 private void onPrevClicked(View view) { 297 PlaybackViewModel.PlaybackStateWrapper state = getPlaybackState(); 298 if ((mController != null) && (state != null) && (state.isSkipPreviousEnabled())) { 299 mController.skipToPrevious(); 300 } 301 } 302 getPlaybackState()303 private PlaybackViewModel.PlaybackStateWrapper getPlaybackState() { 304 if (mModel != null) { 305 return mModel.getPlaybackStateWrapper().getValue(); 306 } 307 return null; 308 } 309 310 } 311