• 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.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.support.v4.app.Fragment;
25 import android.support.v7.widget.GridLayoutManager;
26 import android.support.v7.widget.RecyclerView;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.widget.ImageView;
31 import android.widget.ProgressBar;
32 import android.widget.TextView;
33 
34 import com.android.car.media.browse.BrowseAdapter;
35 import com.android.car.media.browse.ContentForwardStrategy;
36 import com.android.car.media.common.GridSpacingItemDecoration;
37 import com.android.car.media.common.MediaItemMetadata;
38 import com.android.car.media.common.MediaSource;
39 import com.android.car.media.widgets.ViewUtils;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.Stack;
44 
45 import androidx.car.widget.PagedListView;
46 
47 /**
48  * A {@link Fragment} that implements the content forward browsing experience.
49  */
50 public class BrowseFragment extends Fragment {
51     private static final String TAG = "BrowseFragment";
52     private static final String TOP_MEDIA_ITEM_KEY = "top_media_item";
53     private static final String MEDIA_SOURCE_PACKAGE_NAME_KEY = "media_source";
54     private static final String BROWSE_STACK_KEY = "browse_stack";
55 
56     private PagedListView mBrowseList;
57     private ProgressBar mProgressBar;
58     private ImageView mErrorIcon;
59     private TextView mErrorMessage;
60     private MediaSource mMediaSource;
61     private BrowseAdapter mBrowseAdapter;
62     private String mMediaSourcePackageName;
63     private MediaItemMetadata mTopMediaItem;
64     private Callbacks mCallbacks;
65     private int mFadeDuration;
66     private int mProgressBarDelay;
67     private Handler mHandler = new Handler();
68     private Stack<MediaItemMetadata> mBrowseStack = new Stack<>();
69     private MediaSource.Observer mBrowseObserver = new MediaSource.Observer() {
70         @Override
71         protected void onBrowseConnected(boolean success) {
72             BrowseFragment.this.onBrowseConnected(success);
73         }
74 
75         @Override
76         protected void onBrowseDisconnected() {
77             BrowseFragment.this.onBrowseDisconnected();
78         }
79     };
80     private BrowseAdapter.Observer mBrowseAdapterObserver = new BrowseAdapter.Observer() {
81         @Override
82         protected void onDirty() {
83             switch (mBrowseAdapter.getState()) {
84                 case LOADING:
85                 case IDLE:
86                     // Still loading... nothing to do.
87                     break;
88                 case LOADED:
89                     stopLoadingIndicator();
90                     mBrowseAdapter.update();
91                     if (mBrowseAdapter.getItemCount() > 0) {
92                         ViewUtils.showViewAnimated(mBrowseList, mFadeDuration);
93                         ViewUtils.hideViewAnimated(mErrorIcon, mFadeDuration);
94                         ViewUtils.hideViewAnimated(mErrorMessage, mFadeDuration);
95                     } else {
96                         mErrorMessage.setText(R.string.nothing_to_play);
97                         ViewUtils.hideViewAnimated(mBrowseList, mFadeDuration);
98                         ViewUtils.hideViewAnimated(mErrorIcon, mFadeDuration);
99                         ViewUtils.showViewAnimated(mErrorMessage, mFadeDuration);
100                     }
101                     break;
102                 case ERROR:
103                     stopLoadingIndicator();
104                     mErrorMessage.setText(R.string.unknown_error);
105                     ViewUtils.hideViewAnimated(mBrowseList, mFadeDuration);
106                     ViewUtils.showViewAnimated(mErrorMessage, mFadeDuration);
107                     ViewUtils.showViewAnimated(mErrorIcon, mFadeDuration);
108                     break;
109             }
110         }
111 
112         @Override
113         protected void onPlayableItemClicked(MediaItemMetadata item) {
114             mCallbacks.onPlayableItemClicked(mMediaSource, item);
115         }
116 
117         @Override
118         protected void onBrowseableItemClicked(MediaItemMetadata item) {
119             navigateInto(item);
120         }
121 
122         @Override
123         protected void onMoreButtonClicked(MediaItemMetadata item) {
124             navigateInto(item);
125         }
126     };
127 
128     /**
129      * Fragment callbacks (implemented by the hosting Activity)
130      */
131     public interface Callbacks {
132         /**
133          * @return a {@link MediaSource} corresponding to the given package name
134          */
getMediaSource(String packageName)135         MediaSource getMediaSource(String packageName);
136 
137         /**
138          * Method invoked when the back stack changes (for example, when the user moves up or down
139          * the media tree)
140          */
onBackStackChanged()141         void onBackStackChanged();
142 
143         /**
144          * Method invoked when the user clicks on a playable item
145          *
146          * @param mediaSource {@link MediaSource} the playable item belongs to
147          * @param item item to be played.
148          */
onPlayableItemClicked(MediaSource mediaSource, MediaItemMetadata item)149         void onPlayableItemClicked(MediaSource mediaSource, MediaItemMetadata item);
150     }
151 
152     /**
153      * Moves the user one level up in the browse tree, if possible.
154      */
navigateBack()155     public void navigateBack() {
156         mBrowseStack.pop();
157         if (mBrowseAdapter != null) {
158             mBrowseAdapter.setParentMediaItemId(getCurrentMediaItem());
159         }
160         if (mCallbacks != null) {
161             mCallbacks.onBackStackChanged();
162         }
163     }
164 
165     /**
166      * @return whether the user is in a level other than the top.
167      */
isBackEnabled()168     public boolean isBackEnabled() {
169         return !mBrowseStack.isEmpty();
170     }
171 
172     /**
173      * Creates a new instance of this fragment.
174      *
175      * @param mediaSource media source being displayed
176      * @param item media tree node to display on this fragment.
177      * @return a fully initialized {@link BrowseFragment}
178      */
newInstance(MediaSource mediaSource, MediaItemMetadata item)179     public static BrowseFragment newInstance(MediaSource mediaSource, MediaItemMetadata item) {
180         BrowseFragment fragment = new BrowseFragment();
181         Bundle args = new Bundle();
182         args.putParcelable(TOP_MEDIA_ITEM_KEY, item);
183         args.putString(MEDIA_SOURCE_PACKAGE_NAME_KEY, mediaSource.getPackageName());
184         fragment.setArguments(args);
185         return fragment;
186     }
187 
188     @Override
onCreate(@ullable Bundle savedInstanceState)189     public void onCreate(@Nullable Bundle savedInstanceState) {
190         super.onCreate(savedInstanceState);
191         Bundle arguments = getArguments();
192         if (arguments != null) {
193             mTopMediaItem = arguments.getParcelable(TOP_MEDIA_ITEM_KEY);
194             mMediaSourcePackageName = arguments.getString(MEDIA_SOURCE_PACKAGE_NAME_KEY);
195         }
196         if (savedInstanceState != null) {
197             List<MediaItemMetadata> savedStack =
198                     savedInstanceState.getParcelableArrayList(BROWSE_STACK_KEY);
199             mBrowseStack.clear();
200             if (savedStack != null) {
201                 mBrowseStack.addAll(savedStack);
202             }
203         }
204     }
205 
206     @Override
onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState)207     public View onCreateView(LayoutInflater inflater, final ViewGroup container,
208             Bundle savedInstanceState) {
209         View view = inflater.inflate(R.layout.fragment_browse, container, false);
210         mProgressBar = view.findViewById(R.id.loading_spinner);
211         mProgressBarDelay = getContext().getResources()
212                 .getInteger(R.integer.progress_indicator_delay);
213         mBrowseList = view.findViewById(R.id.browse_list);
214         mErrorIcon = view.findViewById(R.id.error_icon);
215         mErrorMessage = view.findViewById(R.id.error_message);
216         mFadeDuration = getContext().getResources().getInteger(
217                 R.integer.new_album_art_fade_in_duration);
218         int numColumns = getContext().getResources().getInteger(R.integer.num_browse_columns);
219         GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), numColumns);
220         RecyclerView recyclerView = mBrowseList.getRecyclerView();
221         recyclerView.setVerticalFadingEdgeEnabled(true);
222         recyclerView.setFadingEdgeLength(getResources()
223                 .getDimensionPixelSize(R.dimen.car_padding_5));
224         recyclerView.setLayoutManager(gridLayoutManager);
225         recyclerView.addItemDecoration(new GridSpacingItemDecoration(
226                 getResources().getDimensionPixelSize(R.dimen.car_padding_4),
227                 getResources().getDimensionPixelSize(R.dimen.car_keyline_1),
228                 getResources().getDimensionPixelSize(R.dimen.car_keyline_1)
229         ));
230         return view;
231     }
232 
233     @Override
onAttach(Context context)234     public void onAttach(Context context) {
235         super.onAttach(context);
236         mCallbacks = (Callbacks) context;
237     }
238 
239     @Override
onDetach()240     public void onDetach() {
241         super.onDetach();
242         mCallbacks = null;
243     }
244 
245     @Override
onStart()246     public void onStart() {
247         super.onStart();
248         startLoadingIndicator();
249         mMediaSource = mCallbacks.getMediaSource(mMediaSourcePackageName);
250         if (mMediaSource != null) {
251             mMediaSource.subscribe(mBrowseObserver);
252         }
253     }
254 
255     private Runnable mProgressIndicatorRunnable = new Runnable() {
256         @Override
257         public void run() {
258             ViewUtils.showViewAnimated(mProgressBar, mFadeDuration);
259         }
260     };
261 
startLoadingIndicator()262     private void startLoadingIndicator() {
263         // Display the indicator after a certain time, to avoid flashing the indicator constantly,
264         // even when performance is acceptable.
265         mHandler.postDelayed(mProgressIndicatorRunnable, mProgressBarDelay);
266     }
267 
stopLoadingIndicator()268     private void stopLoadingIndicator() {
269         mHandler.removeCallbacks(mProgressIndicatorRunnable);
270         ViewUtils.hideViewAnimated(mProgressBar, mFadeDuration);
271     }
272 
273     @Override
onStop()274     public void onStop() {
275         super.onStop();
276         stopLoadingIndicator();
277         if (mMediaSource != null) {
278             mMediaSource.unsubscribe(mBrowseObserver);
279         }
280         if (mBrowseAdapter != null) {
281             mBrowseAdapter.stop();
282             mBrowseAdapter = null;
283         }
284     }
285 
286     @Override
onSaveInstanceState(@onNull Bundle outState)287     public void onSaveInstanceState(@NonNull Bundle outState) {
288         super.onSaveInstanceState(outState);
289         ArrayList<MediaItemMetadata> stack = new ArrayList<>(mBrowseStack);
290         outState.putParcelableArrayList(BROWSE_STACK_KEY, stack);
291     }
292 
onBrowseConnected(boolean success)293     private void onBrowseConnected(boolean success) {
294         if (mBrowseAdapter != null) {
295             mBrowseAdapter.stop();
296             mBrowseAdapter = null;
297         }
298         if (!success) {
299             ViewUtils.hideViewAnimated(mBrowseList, mFadeDuration);
300             stopLoadingIndicator();
301             mErrorMessage.setText(R.string.cannot_connect_to_app);
302             ViewUtils.showViewAnimated(mErrorIcon, mFadeDuration);
303             ViewUtils.showViewAnimated(mErrorMessage, mFadeDuration);
304             return;
305         }
306         mBrowseAdapter = new BrowseAdapter(getContext(), mMediaSource, getCurrentMediaItem(),
307                 ContentForwardStrategy.DEFAULT_STRATEGY);
308         mBrowseList.setAdapter(mBrowseAdapter);
309         mBrowseList.setDividerVisibilityManager(mBrowseAdapter);
310         mBrowseAdapter.registerObserver(mBrowseAdapterObserver);
311         mBrowseAdapter.start();
312     }
313 
onBrowseDisconnected()314     private void onBrowseDisconnected() {
315         if (mBrowseAdapter != null) {
316             mBrowseAdapter.stop();
317             mBrowseAdapter = null;
318         }
319     }
320 
navigateInto(MediaItemMetadata item)321     private void navigateInto(MediaItemMetadata item) {
322         mBrowseStack.push(item);
323         mBrowseAdapter.setParentMediaItemId(item);
324         mCallbacks.onBackStackChanged();
325     }
326 
327     /**
328      * @return the current item being displayed
329      */
getCurrentMediaItem()330     public MediaItemMetadata getCurrentMediaItem() {
331         if (mBrowseStack.isEmpty()) {
332             return mTopMediaItem;
333         } else {
334             return mBrowseStack.lastElement();
335         }
336     }
337 }
338