• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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