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
17package com.android.car.media;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.os.Bundle;
23import android.os.Handler;
24import android.support.v4.app.Fragment;
25import android.support.v7.widget.GridLayoutManager;
26import android.support.v7.widget.RecyclerView;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.ViewGroup;
30import android.widget.ImageView;
31import android.widget.ProgressBar;
32import android.widget.TextView;
33
34import com.android.car.media.browse.BrowseAdapter;
35import com.android.car.media.browse.ContentForwardStrategy;
36import com.android.car.media.common.GridSpacingItemDecoration;
37import com.android.car.media.common.MediaItemMetadata;
38import com.android.car.media.common.MediaSource;
39import com.android.car.media.widgets.ViewUtils;
40
41import java.util.ArrayList;
42import java.util.List;
43import java.util.Stack;
44
45import androidx.car.widget.PagedListView;
46
47/**
48 * A {@link Fragment} that implements the content forward browsing experience.
49 */
50public 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         */
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         */
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         */
149        void onPlayableItemClicked(MediaSource mediaSource, MediaItemMetadata item);
150    }
151
152    /**
153     * Moves the user one level up in the browse tree, if possible.
154     */
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     */
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     */
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
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
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
234    public void onAttach(Context context) {
235        super.onAttach(context);
236        mCallbacks = (Callbacks) context;
237    }
238
239    @Override
240    public void onDetach() {
241        super.onDetach();
242        mCallbacks = null;
243    }
244
245    @Override
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
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
268    private void stopLoadingIndicator() {
269        mHandler.removeCallbacks(mProgressIndicatorRunnable);
270        ViewUtils.hideViewAnimated(mProgressBar, mFadeDuration);
271    }
272
273    @Override
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
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
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
314    private void onBrowseDisconnected() {
315        if (mBrowseAdapter != null) {
316            mBrowseAdapter.stop();
317            mBrowseAdapter = null;
318        }
319    }
320
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     */
330    public MediaItemMetadata getCurrentMediaItem() {
331        if (mBrowseStack.isEmpty()) {
332            return mTopMediaItem;
333        } else {
334            return mBrowseStack.lastElement();
335        }
336    }
337}
338