1 /* 2 * Copyright 2018 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; 18 19 import android.content.Context; 20 import android.media.session.MediaController; 21 import android.os.Bundle; 22 import android.support.annotation.NonNull; 23 import android.support.constraint.ConstraintLayout; 24 import android.support.constraint.ConstraintSet; 25 import android.support.v4.app.Fragment; 26 import android.support.v7.widget.RecyclerView; 27 import android.transition.Transition; 28 import android.transition.TransitionInflater; 29 import android.transition.TransitionListenerAdapter; 30 import android.transition.TransitionManager; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.widget.ImageView; 35 import android.widget.SeekBar; 36 import android.widget.TextView; 37 38 import com.android.car.media.common.MediaItemMetadata; 39 import com.android.car.media.common.MediaSource; 40 import com.android.car.media.common.PlaybackControls; 41 import com.android.car.media.common.PlaybackModel; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.stream.Collectors; 46 47 import androidx.car.widget.ListItem; 48 import androidx.car.widget.ListItemAdapter; 49 import androidx.car.widget.ListItemProvider; 50 import androidx.car.widget.PagedListView; 51 import androidx.car.widget.TextListItem; 52 53 /** 54 * A {@link Fragment} that implements both the playback and the content forward browsing experience. 55 * It observes a {@link PlaybackModel} and updates its information depending on the currently 56 * playing media source through the {@link android.media.session.MediaSession} API. 57 */ 58 public class PlaybackFragment extends Fragment { 59 private static final String TAG = "PlaybackFragment"; 60 61 private PlaybackModel mModel; 62 private PlaybackControls mPlaybackControls; 63 private QueueItemsAdapter mQueueAdapter; 64 private PagedListView mQueue; 65 private Callbacks mCallbacks; 66 67 private MetadataController mMetadataController; 68 private ConstraintLayout mRootView; 69 70 private boolean mNeedsStateUpdate; 71 private boolean mUpdatesPaused; 72 private boolean mQueueIsVisible; 73 private List<MediaItemMetadata> mQueueItems = new ArrayList<>(); 74 private PlaybackModel.PlaybackObserver mPlaybackObserver = 75 new PlaybackModel.PlaybackObserver() { 76 @Override 77 public void onPlaybackStateChanged() { 78 updateState(); 79 } 80 81 @Override 82 public void onSourceChanged() { 83 updateAccentColor(); 84 updateState(); 85 } 86 87 @Override 88 public void onMetadataChanged() { 89 } 90 }; 91 private ListItemProvider mQueueItemsProvider = new ListItemProvider() { 92 @Override 93 public ListItem get(int position) { 94 if (position < 0 || position >= mQueueItems.size()) { 95 return null; 96 } 97 MediaItemMetadata item = mQueueItems.get(position); 98 TextListItem textListItem = new TextListItem(getContext()); 99 textListItem.setTitle(item.getTitle() != null ? item.getTitle().toString() : null); 100 textListItem.setBody(item.getSubtitle() != null ? item.getSubtitle().toString() : null); 101 textListItem.setOnClickListener(v -> onQueueItemClicked(item)); 102 103 return textListItem; 104 } 105 @Override 106 public int size() { 107 return mQueueItems.size(); 108 } 109 }; 110 private class QueueItemsAdapter extends ListItemAdapter { QueueItemsAdapter(Context context, ListItemProvider itemProvider)111 QueueItemsAdapter(Context context, ListItemProvider itemProvider) { 112 super(context, itemProvider, BackgroundStyle.SOLID); 113 setHasStableIds(true); 114 } 115 refresh()116 void refresh() { 117 // TODO: Perform a diff between current and new content and trigger the proper 118 // RecyclerView updates. 119 this.notifyDataSetChanged(); 120 } 121 122 @Override getItemId(int position)123 public long getItemId(int position) { 124 return mQueueItems.get(position).getQueueId(); 125 } 126 } 127 128 /** 129 * Callbacks this fragment can trigger 130 */ 131 public interface Callbacks { 132 /** 133 * Returns the playback model to use. 134 */ getPlaybackModel()135 PlaybackModel getPlaybackModel(); 136 137 /** 138 * Indicates that the "show queue" button has been clicked 139 */ onQueueButtonClicked()140 void onQueueButtonClicked(); 141 } 142 143 private PlaybackControls.Listener mPlaybackControlsListener = new PlaybackControls.Listener() { 144 @Override 145 public void onToggleQueue() { 146 if (mCallbacks != null) { 147 mCallbacks.onQueueButtonClicked(); 148 } 149 } 150 }; 151 152 @Override onCreateView(@onNull LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState)153 public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container, 154 Bundle savedInstanceState) { 155 View view = inflater.inflate(R.layout.fragment_playback, container, false); 156 mRootView = view.findViewById(R.id.playback_container); 157 mQueue = view.findViewById(R.id.queue_list); 158 159 initPlaybackControls(view.findViewById(R.id.playback_controls)); 160 initQueue(mQueue); 161 initMetadataController(view); 162 return view; 163 } 164 165 @Override onAttach(Context context)166 public void onAttach(Context context) { 167 super.onAttach(context); 168 mCallbacks = (Callbacks) context; 169 } 170 171 @Override onDetach()172 public void onDetach() { 173 super.onDetach(); 174 mCallbacks = null; 175 } 176 initPlaybackControls(PlaybackControls playbackControls)177 private void initPlaybackControls(PlaybackControls playbackControls) { 178 mPlaybackControls = playbackControls; 179 mPlaybackControls.setListener(mPlaybackControlsListener); 180 mPlaybackControls.setAnimationViewGroup(mRootView); 181 } 182 initQueue(PagedListView queueList)183 private void initQueue(PagedListView queueList) { 184 RecyclerView recyclerView = queueList.getRecyclerView(); 185 recyclerView.setVerticalFadingEdgeEnabled(true); 186 recyclerView.setFadingEdgeLength(getResources() 187 .getDimensionPixelSize(R.dimen.car_padding_4)); 188 mQueueAdapter = new QueueItemsAdapter(getContext(), mQueueItemsProvider); 189 queueList.setAdapter(mQueueAdapter); 190 } 191 initMetadataController(View view)192 private void initMetadataController(View view) { 193 ImageView albumArt = view.findViewById(R.id.album_art); 194 TextView title = view.findViewById(R.id.title); 195 TextView subtitle = view.findViewById(R.id.subtitle); 196 SeekBar seekbar = view.findViewById(R.id.seek_bar); 197 TextView time = view.findViewById(R.id.time); 198 mMetadataController = new MetadataController(title, subtitle, time, seekbar, albumArt); 199 } 200 201 @Override onStart()202 public void onStart() { 203 super.onStart(); 204 mModel = mCallbacks.getPlaybackModel(); 205 mMetadataController.setModel(mModel); 206 mPlaybackControls.setModel(mModel); 207 mModel.registerObserver(mPlaybackObserver); 208 } 209 210 @Override onStop()211 public void onStop() { 212 super.onStop(); 213 mModel.unregisterObserver(mPlaybackObserver); 214 mMetadataController.setModel(null); 215 mPlaybackControls.setModel(null); 216 mModel = null; 217 } 218 219 /** 220 * Hides or shows the playback queue 221 */ toggleQueueVisibility()222 public void toggleQueueVisibility() { 223 mQueueIsVisible = !mQueueIsVisible; 224 mPlaybackControls.setQueueVisible(mQueueIsVisible); 225 226 Transition transition = TransitionInflater.from(getContext()).inflateTransition( 227 mQueueIsVisible ? R.transition.queue_in : R.transition.queue_out); 228 transition.addListener(new TransitionListenerAdapter() { 229 230 @Override 231 public void onTransitionStart(Transition transition) { 232 super.onTransitionStart(transition); 233 mUpdatesPaused = true; 234 mMetadataController.pauseUpdates(); 235 } 236 237 @Override 238 public void onTransitionEnd(Transition transition) { 239 mUpdatesPaused = false; 240 if (mNeedsStateUpdate) { 241 updateState(); 242 } 243 mMetadataController.resumeUpdates(); 244 mQueue.getRecyclerView().scrollToPosition(0); 245 } 246 }); 247 TransitionManager.beginDelayedTransition(mRootView, transition); 248 ConstraintSet constraintSet = new ConstraintSet(); 249 constraintSet.clone(mRootView.getContext(), 250 mQueueIsVisible ? R.layout.fragment_playback_with_queue : R.layout.fragment_playback); 251 constraintSet.applyTo(mRootView); 252 } 253 updateState()254 private void updateState() { 255 if (mUpdatesPaused) { 256 mNeedsStateUpdate = true; 257 return; 258 } 259 mNeedsStateUpdate = false; 260 mQueueItems = mModel.getQueue().stream() 261 .filter(item -> item.getTitle() != null) 262 .collect(Collectors.toList()); 263 mQueueAdapter.refresh(); 264 } 265 updateAccentColor()266 private void updateAccentColor() { 267 int defaultColor = getResources().getColor(android.R.color.background_dark, null); 268 MediaSource mediaSource = mModel.getMediaSource(); 269 int color = mediaSource == null ? defaultColor : mediaSource.getAccentColor(defaultColor); 270 // TODO: Update queue color 271 } 272 onQueueItemClicked(MediaItemMetadata item)273 private void onQueueItemClicked(MediaItemMetadata item) { 274 mModel.onSkipToQueueItem(item.getQueueId()); 275 } 276 277 /** 278 * Collapses the playback controls. 279 */ closeOverflowMenu()280 public void closeOverflowMenu() { 281 mPlaybackControls.close(); 282 } 283 } 284