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