/*
* This file is part of Popcorn Time.
*
* Popcorn Time is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Popcorn Time is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Popcorn Time. If not, see <http://www.gnu.org/licenses/>.
*/
package pct.droid.fragments;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v7.graphics.Palette;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.squareup.okhttp.Call;
import java.util.ArrayList;
import butterknife.ButterKnife;
import butterknife.Bind;
import hugo.weaving.DebugLog;
import pct.droid.R;
import pct.droid.activities.MediaDetailActivity;
import pct.droid.adapters.MediaGridAdapter;
import pct.droid.base.PopcornApplication;
import pct.droid.base.content.preferences.Prefs;
import pct.droid.base.providers.media.MediaProvider;
import pct.droid.base.providers.media.models.Media;
import pct.droid.base.utils.LocaleUtils;
import pct.droid.base.utils.PrefUtils;
import pct.droid.base.utils.ThreadUtils;
import pct.droid.fragments.dialog.LoadingDetailDialogFragment;
import timber.log.Timber;
/**
* This fragment is the main screen for viewing a collection of media items.
* <p/>
* LOADING
* <p/>
* This fragment has 2 ways of representing a loading state; If the data is being loaded for the first time, or the media detail for the
* detail screen is being loaded,a progress layout is displayed with a message.
* <p/>
* If a page is being loaded, the adapter will display a progress item.
* <p/>
* MODE
* <p/>
* This fragment can be instantiated with ether a SEARCH mode, or a NORMAL mode. SEARCH mode simply does not load any initial data.
*/
public class MediaListFragment extends Fragment implements LoadingDetailDialogFragment.Callback {
public static final String EXTRA_PROVIDER = "extra_provider";
public static final String EXTRA_SORT = "extra_sort";
public static final String EXTRA_ORDER = "extra_order";
public static final String EXTRA_GENRE = "extra_genre";
public static final String EXTRA_MODE = "extra_mode";
public static final String DIALOG_LOADING_DETAIL = "DIALOG_LOADING_DETAIL";
public static final int LOADING_DIALOG_FRAGMENT = 1;
private Context mContext;
private MediaGridAdapter mAdapter;
private GridLayoutManager mLayoutManager;
private Integer mColumns = 2, mRetries = 0;
//overrides the default loading message
private int mLoadingMessage = R.string.loading_data;
private State mState = State.UNINITIALISED;
private Mode mMode;
private MediaProvider.Filters.Sort mSort;
private MediaProvider.Filters.Order mDefOrder;
public enum Mode {
NORMAL, SEARCH
}
private enum State {
UNINITIALISED, LOADING, SEARCHING, LOADING_PAGE, LOADED, LOADING_DETAIL
}
private ArrayList<Media> mItems = new ArrayList<>();
private boolean mEndOfListReached = false;
private int mFirstVisibleItem, mVisibleItemCount, mTotalItemCount = 0, mLoadingTreshold = mColumns * 3, mPreviousTotal = 0;
private MediaProvider mProvider;
private Call mCurrentCall;
private int mPage = 1;
private MediaProvider.Filters mFilters = new MediaProvider.Filters();
private String mGenre;
View mRootView;
@Bind(R.id.progressOverlay)
LinearLayout mProgressOverlay;
@Bind(R.id.recyclerView)
RecyclerView mRecyclerView;
@Bind(R.id.emptyView)
TextView mEmptyView;
@Bind(R.id.progress_textview)
TextView mProgressTextView;
public static MediaListFragment newInstance(Mode mode, MediaProvider provider, MediaProvider.Filters.Sort filter, MediaProvider.Filters.Order defOrder) {
return newInstance(mode, provider, filter, defOrder, null);
}
public static MediaListFragment newInstance(Mode mode, MediaProvider provider, MediaProvider.Filters.Sort filter, MediaProvider.Filters.Order defOrder, String genre) {
MediaListFragment frag = new MediaListFragment();
Bundle args = new Bundle();
args.putParcelable(EXTRA_PROVIDER, provider);
args.putSerializable(EXTRA_MODE, mode);
args.putSerializable(EXTRA_SORT, filter);
args.putSerializable(EXTRA_ORDER, defOrder);
args.putString(EXTRA_GENRE, genre);
frag.setArguments(args);
return frag;
}
public void changeGenre(String genre) {
if (!(mFilters.genre == null ? "" : mFilters.genre).equals(genre == null ? "" : genre)) {
if(mCurrentCall != null)
PopcornApplication.getHttpClient().getDispatcher().getExecutorService().execute(new Runnable() {
@Override
public void run() {
mCurrentCall.cancel();
}
});
mAdapter.clearItems();
mGenre = mFilters.genre = genre;
mFilters.page = 1;
mCurrentCall = mProvider.getList(new MediaProvider.Filters(mFilters), mCallback);
setState(State.LOADING);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mContext = getActivity();
mRootView = inflater.inflate(R.layout.fragment_media, container, false);
ButterKnife.bind(this, mRootView);
mColumns = getResources().getInteger(R.integer.overview_cols);
mLoadingTreshold = mColumns * 3;
mLayoutManager = new GridLayoutManager(mContext, mColumns);
mRecyclerView.setLayoutManager(mLayoutManager);
return mRootView;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.addOnScrollListener(mScrollListener);
//adapter should only ever be created once on fragment initialise.
mAdapter = new MediaGridAdapter(mContext, mItems, mColumns);
mAdapter.setOnItemClickListener(mOnItemClickListener);
mRecyclerView.setAdapter(mAdapter);
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
mAdapter.setOnItemClickListener(mOnItemClickListener);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//get the provider type and create a provider
mProvider = getArguments().getParcelable(EXTRA_PROVIDER);
mSort = (MediaProvider.Filters.Sort) getArguments().getSerializable(EXTRA_SORT);
mDefOrder = (MediaProvider.Filters.Order) getArguments().getSerializable(EXTRA_ORDER);
mFilters.sort = mSort;
// if not changed use default order
mFilters.order = mDefOrder;
mFilters.genre = getArguments().getString(EXTRA_GENRE);
String language = PrefUtils.get(getActivity(), Prefs.LOCALE, PopcornApplication.getSystemLanguage());
mFilters.langCode = LocaleUtils.toLocale(language).getLanguage();
mMode = (Mode) getArguments().getSerializable(EXTRA_MODE);
if (mMode == Mode.SEARCH) mEmptyView.setText(getString(R.string.no_search_results));
//don't load initial data in search mode
if (mMode != Mode.SEARCH && mAdapter.getItemCount() == 0) {
mCurrentCall = mProvider.getList(new MediaProvider.Filters(mFilters), mCallback);/* fetch new items */
setState(State.LOADING);
} else updateUI();
}
/**
* Responsible for updating the UI based on the state of this fragment
*/
private void updateUI() {
if (!isAdded()) return;
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
//animate recyclerview to full alpha
// if (mRecyclerView.getAlpha() != 1.0f)
// mRecyclerView.animate().alpha(1.0f).setDuration(100).start();
//update loading message based on state
switch (mState) {
case LOADING_DETAIL:
mLoadingMessage = R.string.loading_details;
break;
case SEARCHING:
mLoadingMessage = R.string.searching;
break;
default:
int providerMessage = mProvider.getLoadingMessage();
mLoadingMessage = providerMessage > 0 ? providerMessage : R.string.loading_data;
break;
}
switch (mState) {
case LOADING_DETAIL:
case SEARCHING:
case LOADING:
if (mAdapter.isLoading()) mAdapter.removeLoading();
//show the progress bar
mRecyclerView.setVisibility(View.VISIBLE);
// mRecyclerView.animate().alpha(0.5f).setDuration(500).start();
mEmptyView.setVisibility(View.GONE);
mProgressOverlay.setVisibility(View.VISIBLE);
break;
case LOADED:
if (mAdapter.isLoading()) mAdapter.removeLoading();
mProgressOverlay.setVisibility(View.GONE);
boolean hasItems = mItems.size() > 0;
//show either the recyclerview or the empty view
mRecyclerView.setVisibility(hasItems ? View.VISIBLE : View.INVISIBLE);
mEmptyView.setVisibility(hasItems ? View.GONE : View.VISIBLE);
break;
case LOADING_PAGE:
//add a loading view to the adapter
if (!mAdapter.isLoading()) mAdapter.addLoading();
mEmptyView.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
break;
}
updateLoadingMessage();
}
});
}
private void updateLoadingMessage() {
mProgressTextView.setText(mLoadingMessage);
}
@DebugLog
private void setState(State state) {
if (mState == state) return;//do nothing
mState = state;
updateUI();
}
public void triggerSearch(String searchQuery) {
if (!isAdded()) return;
if (null == mAdapter) return;
if(mCurrentCall != null)
PopcornApplication.getHttpClient().getDispatcher().getExecutorService().execute(new Runnable() {
@Override
public void run() {
mCurrentCall.cancel();
}
});
mEndOfListReached = false;
mItems.clear();
mAdapter.clearItems();//clear out adapter
if (searchQuery.equals("")) {
setState(State.LOADED);
return; //don't do a search for empty queries
}
setState(State.SEARCHING);
mFilters.keywords = searchQuery;
mFilters.page = 1;
mPage = 1;
mCurrentCall = mProvider.getList(new MediaProvider.Filters(mFilters), mCallback);
}
private MediaProvider.Callback mCallback = new MediaProvider.Callback() {
@Override
@DebugLog
public void onSuccess(MediaProvider.Filters filters, final ArrayList<Media> items, boolean changed) {
if (!(mGenre == null ? "" : mGenre).equals(filters.genre == null ? "" : filters.genre)) return; // nothing changed according to the provider, so don't do anything
if(!changed) {
setState(State.LOADED);
return;
}
mItems.clear();
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
setState(State.LOADED);
}
});
if (null != items) mItems.addAll(items);
//fragment may be detached, so we dont want to update the UI
if (!isAdded())
return;
mEndOfListReached = false;
mPage = mPage + 1;
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mAdapter.setItems(items);
mPreviousTotal = mTotalItemCount = mAdapter.getItemCount();
}
});
}
@Override
@DebugLog
public void onFailure(Exception e) {
if (isDetached() || e.getMessage().equals("Canceled")) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
if (mAdapter == null) {
return;
}
mAdapter.removeLoading();
setState(State.LOADED);
}
});
} else if (e.getMessage() != null && e.getMessage().equals(PopcornApplication.getAppContext().getString(R.string.movies_error))) {
mEndOfListReached = true;
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
if (mAdapter == null) {
return;
}
mAdapter.removeLoading();
setState(State.LOADED);
}
});
} else {
e.printStackTrace();
Timber.e(e.getMessage());
if (mRetries > 1) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
Snackbar.make(mRootView, R.string.unknown_error, Snackbar.LENGTH_SHORT).show();
setState(State.LOADED);
}
});
} else {
mCurrentCall = mProvider.getList(mItems, new MediaProvider.Filters(mFilters), this);
}
mRetries++;
}
}
};
private MediaGridAdapter.OnItemClickListener mOnItemClickListener = new MediaGridAdapter.OnItemClickListener() {
@Override
public void onItemClick(final View view, final Media item, final int position) {
/**
* We shouldn't really be doing the palette loading here without any ui feedback,
* but it should be really quick
*/
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view);
if (holder instanceof MediaGridAdapter.ViewHolder) {
ImageView coverImage = ((MediaGridAdapter.ViewHolder) holder).getCoverImage();
if (coverImage.getDrawable() == null) {
showLoadingDialog(position);
return;
}
Bitmap cover = ((BitmapDrawable) coverImage.getDrawable()).getBitmap();
Palette.generateAsync(cover, 5, new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
int vibrantColor = palette.getVibrantColor(-1);
int paletteColor;
if (vibrantColor == -1) {
paletteColor = palette.getMutedColor(getResources().getColor(R.color.primary));
} else {
paletteColor = vibrantColor;
}
item.color = paletteColor;
showLoadingDialog(position);
}
});
} else {
showLoadingDialog(position);
}
}
};
private void showLoadingDialog(Integer position) {
LoadingDetailDialogFragment loadingFragment = LoadingDetailDialogFragment.newInstance(position);
loadingFragment.setTargetFragment(MediaListFragment.this, LOADING_DIALOG_FRAGMENT);
loadingFragment.show(getFragmentManager(), DIALOG_LOADING_DETAIL);
}
private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mVisibleItemCount = mLayoutManager.getChildCount();
mTotalItemCount = mLayoutManager.getItemCount() - (mAdapter.isLoading() ? 1 : 0);
mFirstVisibleItem = mLayoutManager.findFirstVisibleItemPosition();
if (mState == State.LOADING_PAGE) {
if (mTotalItemCount > mPreviousTotal) {
mPreviousTotal = mTotalItemCount;
mPreviousTotal = mTotalItemCount = mLayoutManager.getItemCount();
setState(State.LOADED);
}
}
if (!mEndOfListReached && !(mState == State.SEARCHING) && !(mState == State.LOADING_PAGE) && !(mState == State.LOADING) && (mTotalItemCount - mVisibleItemCount) <= (mFirstVisibleItem +
mLoadingTreshold)) {
mFilters.page = mPage;
mCurrentCall = mProvider.getList(mItems, new MediaProvider.Filters(mFilters), mCallback);
mPreviousTotal = mTotalItemCount = mLayoutManager.getItemCount();
setState(State.LOADING_PAGE);
}
}
};
/**
* Called when loading media details fails
*/
@Override
public void onDetailLoadFailure() {
Snackbar.make(mRootView, R.string.unknown_error, Snackbar.LENGTH_SHORT).show();
}
/**
* Called when media details have been loaded. This should be called on a background thread.
*
* @param item
*/
@Override
public void onDetailLoadSuccess(final Media item) {
MediaDetailActivity.startActivity(mContext, item);
}
/**
* Called when loading media details
* @return mItems
*/
@Override
public ArrayList<Media> getCurrentList() {
return mItems;
}
}