1 /* 2 * Copyright (C) 2020 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.systemui.media; 18 19 import android.app.PendingIntent; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.res.ColorStateList; 23 import android.graphics.Outline; 24 import android.graphics.Rect; 25 import android.graphics.drawable.Drawable; 26 import android.graphics.drawable.Icon; 27 import android.media.session.MediaController; 28 import android.media.session.MediaSession; 29 import android.media.session.PlaybackState; 30 import android.util.Log; 31 import android.view.View; 32 import android.view.ViewOutlineProvider; 33 import android.widget.ImageButton; 34 import android.widget.ImageView; 35 import android.widget.TextView; 36 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 import androidx.annotation.UiThread; 40 import androidx.constraintlayout.widget.ConstraintSet; 41 42 import com.android.settingslib.Utils; 43 import com.android.settingslib.media.MediaOutputSliceConstants; 44 import com.android.settingslib.widget.AdaptiveIcon; 45 import com.android.systemui.R; 46 import com.android.systemui.dagger.qualifiers.Background; 47 import com.android.systemui.plugins.ActivityStarter; 48 import com.android.systemui.util.animation.TransitionLayout; 49 50 import java.util.List; 51 import java.util.concurrent.Executor; 52 53 import javax.inject.Inject; 54 55 /** 56 * A view controller used for Media Playback. 57 */ 58 public class MediaControlPanel { 59 private static final String TAG = "MediaControlPanel"; 60 private static final float DISABLED_ALPHA = 0.38f; 61 62 // Button IDs for QS controls 63 static final int[] ACTION_IDS = { 64 R.id.action0, 65 R.id.action1, 66 R.id.action2, 67 R.id.action3, 68 R.id.action4 69 }; 70 71 private final SeekBarViewModel mSeekBarViewModel; 72 private SeekBarObserver mSeekBarObserver; 73 protected final Executor mBackgroundExecutor; 74 private final ActivityStarter mActivityStarter; 75 76 private Context mContext; 77 private PlayerViewHolder mViewHolder; 78 private MediaViewController mMediaViewController; 79 private MediaSession.Token mToken; 80 private MediaController mController; 81 private int mBackgroundColor; 82 private int mAlbumArtSize; 83 private int mAlbumArtRadius; 84 // This will provide the corners for the album art. 85 private final ViewOutlineProvider mViewOutlineProvider; 86 87 /** 88 * Initialize a new control panel 89 * @param context 90 * @param backgroundExecutor background executor, used for processing artwork 91 * @param activityStarter activity starter 92 */ 93 @Inject MediaControlPanel(Context context, @Background Executor backgroundExecutor, ActivityStarter activityStarter, MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel)94 public MediaControlPanel(Context context, @Background Executor backgroundExecutor, 95 ActivityStarter activityStarter, MediaViewController mediaViewController, 96 SeekBarViewModel seekBarViewModel) { 97 mContext = context; 98 mBackgroundExecutor = backgroundExecutor; 99 mActivityStarter = activityStarter; 100 mSeekBarViewModel = seekBarViewModel; 101 mMediaViewController = mediaViewController; 102 loadDimens(); 103 104 mViewOutlineProvider = new ViewOutlineProvider() { 105 @Override 106 public void getOutline(View view, Outline outline) { 107 outline.setRoundRect(0, 0, mAlbumArtSize, mAlbumArtSize, mAlbumArtRadius); 108 } 109 }; 110 } 111 onDestroy()112 public void onDestroy() { 113 if (mSeekBarObserver != null) { 114 mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver); 115 } 116 mSeekBarViewModel.onDestroy(); 117 mMediaViewController.onDestroy(); 118 } 119 loadDimens()120 private void loadDimens() { 121 mAlbumArtRadius = mContext.getResources().getDimensionPixelSize( 122 Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius)); 123 mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size); 124 } 125 126 /** 127 * Get the view holder used to display media controls 128 * @return the view holder 129 */ 130 @Nullable getView()131 public PlayerViewHolder getView() { 132 return mViewHolder; 133 } 134 135 /** 136 * Get the view controller used to display media controls 137 * @return the media view controller 138 */ 139 @NonNull getMediaViewController()140 public MediaViewController getMediaViewController() { 141 return mMediaViewController; 142 } 143 144 /** 145 * Sets the listening state of the player. 146 * 147 * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid 148 * unnecessary work when the QS panel is closed. 149 * 150 * @param listening True when player should be active. Otherwise, false. 151 */ setListening(boolean listening)152 public void setListening(boolean listening) { 153 mSeekBarViewModel.setListening(listening); 154 } 155 156 /** 157 * Get the context 158 * @return context 159 */ getContext()160 public Context getContext() { 161 return mContext; 162 } 163 164 /** Attaches the player to the view holder. */ attach(PlayerViewHolder vh)165 public void attach(PlayerViewHolder vh) { 166 mViewHolder = vh; 167 TransitionLayout player = vh.getPlayer(); 168 169 ImageView albumView = vh.getAlbumView(); 170 albumView.setOutlineProvider(mViewOutlineProvider); 171 albumView.setClipToOutline(true); 172 173 mSeekBarObserver = new SeekBarObserver(vh); 174 mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); 175 mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar()); 176 mMediaViewController.attach(player); 177 } 178 179 /** 180 * Bind this view based on the data given 181 */ bind(@onNull MediaData data)182 public void bind(@NonNull MediaData data) { 183 if (mViewHolder == null) { 184 return; 185 } 186 MediaSession.Token token = data.getToken(); 187 mBackgroundColor = data.getBackgroundColor(); 188 if (mToken == null || !mToken.equals(token)) { 189 mToken = token; 190 } 191 192 if (mToken != null) { 193 mController = new MediaController(mContext, mToken); 194 } else { 195 mController = null; 196 } 197 198 ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); 199 ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); 200 201 mViewHolder.getPlayer().setBackgroundTintList( 202 ColorStateList.valueOf(mBackgroundColor)); 203 204 // Click action 205 PendingIntent clickIntent = data.getClickIntent(); 206 if (clickIntent != null) { 207 mViewHolder.getPlayer().setOnClickListener(v -> { 208 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent); 209 }); 210 } 211 212 ImageView albumView = mViewHolder.getAlbumView(); 213 boolean hasArtwork = data.getArtwork() != null; 214 if (hasArtwork) { 215 Drawable artwork = scaleDrawable(data.getArtwork()); 216 albumView.setImageDrawable(artwork); 217 } 218 setVisibleAndAlpha(collapsedSet, R.id.album_art, hasArtwork); 219 setVisibleAndAlpha(expandedSet, R.id.album_art, hasArtwork); 220 221 // App icon 222 ImageView appIcon = mViewHolder.getAppIcon(); 223 if (data.getAppIcon() != null) { 224 appIcon.setImageDrawable(data.getAppIcon()); 225 } else { 226 Drawable iconDrawable = mContext.getDrawable(R.drawable.ic_music_note); 227 appIcon.setImageDrawable(iconDrawable); 228 } 229 230 // Song name 231 TextView titleText = mViewHolder.getTitleText(); 232 titleText.setText(data.getSong()); 233 234 // App title 235 TextView appName = mViewHolder.getAppName(); 236 appName.setText(data.getApp()); 237 238 // Artist name 239 TextView artistText = mViewHolder.getArtistText(); 240 artistText.setText(data.getArtist()); 241 242 // Transfer chip 243 mViewHolder.getSeamless().setVisibility(View.VISIBLE); 244 setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */); 245 setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */); 246 mViewHolder.getSeamless().setOnClickListener(v -> { 247 final Intent intent = new Intent() 248 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) 249 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, 250 data.getPackageName()) 251 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken); 252 mActivityStarter.startActivity(intent, false, true /* dismissShade */, 253 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 254 }); 255 256 ImageView iconView = mViewHolder.getSeamlessIcon(); 257 TextView deviceName = mViewHolder.getSeamlessText(); 258 259 final MediaDeviceData device = data.getDevice(); 260 final int seamlessId = mViewHolder.getSeamless().getId(); 261 final int seamlessFallbackId = mViewHolder.getSeamlessFallback().getId(); 262 final boolean showFallback = device != null && !device.getEnabled(); 263 final int seamlessFallbackVisibility = showFallback ? View.VISIBLE : View.GONE; 264 mViewHolder.getSeamlessFallback().setVisibility(seamlessFallbackVisibility); 265 expandedSet.setVisibility(seamlessFallbackId, seamlessFallbackVisibility); 266 collapsedSet.setVisibility(seamlessFallbackId, seamlessFallbackVisibility); 267 final int seamlessVisibility = showFallback ? View.GONE : View.VISIBLE; 268 mViewHolder.getSeamless().setVisibility(seamlessVisibility); 269 expandedSet.setVisibility(seamlessId, seamlessVisibility); 270 collapsedSet.setVisibility(seamlessId, seamlessVisibility); 271 final float seamlessAlpha = data.getResumption() ? DISABLED_ALPHA : 1.0f; 272 expandedSet.setAlpha(seamlessId, seamlessAlpha); 273 collapsedSet.setAlpha(seamlessId, seamlessAlpha); 274 // Disable clicking on output switcher for resumption controls. 275 mViewHolder.getSeamless().setEnabled(!data.getResumption()); 276 if (showFallback) { 277 iconView.setImageDrawable(null); 278 deviceName.setText(null); 279 } else if (device != null) { 280 Drawable icon = device.getIcon(); 281 iconView.setVisibility(View.VISIBLE); 282 if (icon instanceof AdaptiveIcon) { 283 AdaptiveIcon aIcon = (AdaptiveIcon) icon; 284 aIcon.setBackgroundColor(mBackgroundColor); 285 iconView.setImageDrawable(aIcon); 286 } else { 287 iconView.setImageDrawable(icon); 288 } 289 deviceName.setText(device.getName()); 290 } else { 291 // Reset to default 292 Log.w(TAG, "device is null. Not binding output chip."); 293 iconView.setVisibility(View.GONE); 294 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action); 295 } 296 297 List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact(); 298 // Media controls 299 int i = 0; 300 List<MediaAction> actionIcons = data.getActions(); 301 for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) { 302 int actionId = ACTION_IDS[i]; 303 final ImageButton button = mViewHolder.getAction(actionId); 304 MediaAction mediaAction = actionIcons.get(i); 305 button.setImageDrawable(mediaAction.getDrawable()); 306 button.setContentDescription(mediaAction.getContentDescription()); 307 Runnable action = mediaAction.getAction(); 308 309 if (action == null) { 310 button.setEnabled(false); 311 } else { 312 button.setEnabled(true); 313 button.setOnClickListener(v -> { 314 action.run(); 315 }); 316 } 317 boolean visibleInCompat = actionsWhenCollapsed.contains(i); 318 setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat); 319 setVisibleAndAlpha(expandedSet, actionId, true /*visible */); 320 } 321 322 // Hide any unused buttons 323 for (; i < ACTION_IDS.length; i++) { 324 setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */); 325 setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */); 326 } 327 328 // Seek Bar 329 final MediaController controller = getController(); 330 mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller)); 331 332 // Set up long press menu 333 // TODO: b/156036025 bring back media guts 334 335 // TODO: We don't need to refresh this state constantly, only if the state actually changed 336 // to something which might impact the measurement 337 mMediaViewController.refreshState(); 338 } 339 340 @UiThread scaleDrawable(Icon icon)341 private Drawable scaleDrawable(Icon icon) { 342 if (icon == null) { 343 return null; 344 } 345 // Let's scale down the View, such that the content always nicely fills the view. 346 // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect 347 // ratios 348 Drawable drawable = icon.loadDrawable(mContext); 349 float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth(); 350 Rect bounds; 351 if (aspectRatio > 1.0f) { 352 bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio)); 353 } else { 354 bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize); 355 } 356 if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) { 357 float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f; 358 float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f; 359 bounds.offset((int) -offsetX,(int) -offsetY); 360 } 361 drawable.setBounds(bounds); 362 return drawable; 363 } 364 365 /** 366 * Get the current media controller 367 * @return the controller 368 */ getController()369 public MediaController getController() { 370 return mController; 371 } 372 373 /** 374 * Check whether the media controlled by this player is currently playing 375 * @return whether it is playing, or false if no controller information 376 */ isPlaying()377 public boolean isPlaying() { 378 return isPlaying(mController); 379 } 380 381 /** 382 * Check whether the given controller is currently playing 383 * @param controller media controller to check 384 * @return whether it is playing, or false if no controller information 385 */ isPlaying(MediaController controller)386 protected boolean isPlaying(MediaController controller) { 387 if (controller == null) { 388 return false; 389 } 390 391 PlaybackState state = controller.getPlaybackState(); 392 if (state == null) { 393 return false; 394 } 395 396 return (state.getState() == PlaybackState.STATE_PLAYING); 397 } 398 setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible)399 private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) { 400 set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE); 401 set.setAlpha(actionId, visible ? 1.0f : 0.0f); 402 } 403 } 404