1 package com.android.car.media; 2 3 import android.annotation.NonNull; 4 import android.annotation.Nullable; 5 import android.media.session.PlaybackState; 6 import android.support.v4.media.session.PlaybackStateCompat; 7 import android.view.View; 8 import android.widget.ImageView; 9 import android.widget.SeekBar; 10 import android.widget.TextView; 11 12 import com.android.car.media.common.MediaItemMetadata; 13 import com.android.car.media.common.PlaybackModel; 14 15 import java.text.DateFormat; 16 import java.text.SimpleDateFormat; 17 import java.util.Date; 18 import java.util.Locale; 19 20 /** 21 * Common controller for displaying current track's metadata. 22 */ 23 public class MetadataController { 24 25 private static final DateFormat TIME_FORMAT = new SimpleDateFormat("m:ss", Locale.US); 26 27 @NonNull 28 private final TextView mTitle; 29 @NonNull 30 private final TextView mSubtitle; 31 @Nullable 32 private final TextView mTime; 33 @NonNull 34 private final SeekBar mSeekBar; 35 @Nullable 36 private final ImageView mAlbumArt; 37 38 @Nullable 39 private PlaybackModel mModel; 40 41 private boolean mUpdatesPaused; 42 private boolean mNeedsMetadataUpdate; 43 private int mAlbumArtSize; 44 45 private final PlaybackModel.PlaybackObserver mPlaybackObserver = 46 new PlaybackModel.PlaybackObserver() { 47 @Override 48 public void onPlaybackStateChanged() { 49 updateState(); 50 } 51 52 @Override 53 public void onSourceChanged() { 54 updateState(); 55 updateMetadata(); 56 } 57 58 @Override 59 public void onMetadataChanged() { 60 updateMetadata(); 61 } 62 }; 63 64 /** 65 * Create a new MetadataController that operates on the provided Views 66 * @param title Displays the track's title. Must not be {@code null}. 67 * @param subtitle Displays the track's artist. Must not be {@code null}. 68 * @param time Displays the track's progress as text. May be {@code null}. 69 * @param seekBar Displays the track's progress visually. Must not be {@code null}. 70 * @param albumArt Displays the track's album art. May be {@code null}. 71 */ MetadataController(@onNull TextView title, @NonNull TextView subtitle, @Nullable TextView time, @NonNull SeekBar seekBar, @Nullable ImageView albumArt)72 public MetadataController(@NonNull TextView title, @NonNull TextView subtitle, 73 @Nullable TextView time, @NonNull SeekBar seekBar, @Nullable ImageView albumArt) { 74 mTitle = title; 75 mSubtitle = subtitle; 76 mTime = time; 77 mSeekBar = seekBar; 78 mSeekBar.setOnTouchListener((view, event) -> true); 79 mAlbumArt = albumArt; 80 mAlbumArtSize = title.getContext().getResources() 81 .getDimensionPixelSize(R.dimen.playback_album_art_size_large); 82 } 83 84 /** 85 * Registers the {@link PlaybackModel} this widget will use to follow playback state. 86 * Consumers of this class must unregister the {@link PlaybackModel} by calling this method with 87 * null. 88 * 89 * @param model {@link PlaybackModel} to subscribe, or null to unsubscribe. 90 */ setModel(@ullable PlaybackModel model)91 public void setModel(@Nullable PlaybackModel model) { 92 if (mModel != null) { 93 mModel.unregisterObserver(mPlaybackObserver); 94 } 95 mModel = model; 96 if (mModel != null) { 97 mModel.registerObserver(mPlaybackObserver); 98 } 99 } 100 updateState()101 private void updateState() { 102 updateProgress(); 103 104 mSeekBar.removeCallbacks(mSeekBarRunnable); 105 if (mModel != null && mModel.isPlaying()) { 106 mSeekBar.post(mSeekBarRunnable); 107 } 108 } 109 updateMetadata()110 private void updateMetadata() { 111 if(mUpdatesPaused) { 112 mNeedsMetadataUpdate = true; 113 return; 114 } 115 116 mNeedsMetadataUpdate = false; 117 MediaItemMetadata metadata = mModel != null ? mModel.getMetadata() : null; 118 mTitle.setText(metadata != null ? metadata.getTitle() : null); 119 mSubtitle.setText(metadata != null ? metadata.getSubtitle() : null); 120 if (mAlbumArt != null && metadata != null && (metadata.getAlbumArtUri() != null 121 || metadata.getAlbumArtBitmap() != null)) { 122 mAlbumArt.setVisibility(View.VISIBLE); 123 metadata.getAlbumArt(mAlbumArt.getContext(), mAlbumArtSize, mAlbumArtSize, true) 124 .thenAccept(mAlbumArt::setImageBitmap); 125 } else if (mAlbumArt != null) { 126 mAlbumArt.setVisibility(View.GONE); 127 } 128 } 129 130 private static final long SEEK_BAR_UPDATE_TIME_INTERVAL_MS = 1000; 131 132 private final Runnable mSeekBarRunnable = new Runnable() { 133 @Override 134 public void run() { 135 if (mModel == null || !mModel.isPlaying()) { 136 return; 137 } 138 updateProgress(); 139 mSeekBar.postDelayed(this, SEEK_BAR_UPDATE_TIME_INTERVAL_MS); 140 141 } 142 }; 143 updateProgress()144 private void updateProgress() { 145 if (mModel == null) { 146 mTime.setVisibility(View.INVISIBLE); 147 mSeekBar.setVisibility(View.INVISIBLE); 148 return; 149 } 150 long maxProgress = mModel.getMaxProgress(); 151 long progress = mModel.getProgress(); 152 int visibility = maxProgress > 0 && progress != PlaybackState.PLAYBACK_POSITION_UNKNOWN 153 ? View.VISIBLE : View.INVISIBLE; 154 if (mTime != null) { 155 String time = String.format("%s / %s", 156 TIME_FORMAT.format(new Date(progress)), 157 TIME_FORMAT.format(new Date(maxProgress))); 158 mTime.setVisibility(visibility); 159 mTime.setText(time); 160 } 161 mSeekBar.setVisibility(visibility); 162 mSeekBar.setMax((int) maxProgress); 163 mSeekBar.setProgress((int) progress); 164 } 165 166 pauseUpdates()167 public void pauseUpdates() { 168 mUpdatesPaused = true; 169 } 170 resumeUpdates()171 public void resumeUpdates() { 172 mUpdatesPaused = false; 173 if (mNeedsMetadataUpdate) { 174 updateMetadata(); 175 } 176 } 177 } 178