/**
* Copyright (C) 2013 Romain Guefveneu.
*
* This file is part of naonedbus.
*
* Naonedbus 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.
*
* Naonedbus 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.naonedbus.fragment;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import net.naonedbus.BuildConfig;
import net.naonedbus.R;
import net.naonedbus.bean.async.AsyncResult;
import net.naonedbus.widget.PinnedHeaderListView;
import org.joda.time.DateTime;
import org.json.JSONException;
import android.content.Context;
import android.database.DataSetObserver;
import android.os.Bundle;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.animation.AnimationUtils;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.Adapter;
import android.widget.Button;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.actionbarsherlock.app.SherlockListFragment;
import de.keyboardsurfer.android.widget.crouton.Crouton;
import de.keyboardsurfer.android.widget.crouton.Style;
public abstract class CustomListFragment extends SherlockListFragment implements
LoaderCallbacks<AsyncResult<ListAdapter>> {
private static enum State {
CONTENT, LOADER, MESSAGE;
}
private static final int LOADER_INIT = 0;
private static final int LOADER_REFRESH = 1;
private static final String STATE_POSITION = "position";
private static final String STATE_TOP = "top";
private static final String LOG_TAG = "CustomListFragment";
private static final boolean DBG = BuildConfig.DEBUG;
int mMessageEmptyTitleId = R.string.error_title_empty;
int mMessageEmptySummaryId = R.string.error_summary_empty;
int mMessageEmptyDrawableId = R.drawable.ic_sad_face;
protected int mLayoutId;
protected int mLayoutListHeaderId = R.layout.list_item_header;
protected ViewGroup mFragmentView;
private int mListViewStatePosition;
private int mListViewStateTop;
private final List<OnScrollListener> mOnScrollListeners = new ArrayList<AbsListView.OnScrollListener>();
private State mCurrentState;
private DateTime mNextUpdate = null;
/** Minutes pendant lesquelles le contenu est considéré comme à jour. */
private int mTimeToLive = 5;
public CustomListFragment(final int layoutId) {
mLayoutId = layoutId;
}
public CustomListFragment(final int layoutId, final int layoutListHeaderId) {
this(layoutId);
mLayoutListHeaderId = layoutListHeaderId;
}
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "onActivityCreated " + mCurrentState);
if (mCurrentState == State.MESSAGE) {
mCurrentState = null;
if (getListAdapter() == null || getListAdapter().getCount() == 0) {
showMessage(mMessageEmptyTitleId, mMessageEmptySummaryId, mMessageEmptyDrawableId);
}
}
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
if (container == null) // must put this in
return null;
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "onCreateView " + mCurrentState);
if (savedInstanceState != null) {
mListViewStatePosition = savedInstanceState.getInt(STATE_POSITION, -1);
mListViewStateTop = savedInstanceState.getInt(STATE_TOP, 0);
} else {
mListViewStatePosition = -1;
mListViewStateTop = 0;
}
mFragmentView = (ViewGroup) inflater.inflate(R.layout.fragment_base, container, false);
final View view = inflater.inflate(mLayoutId, container, false);
bindView(view, savedInstanceState);
mFragmentView.addView(view);
setupListView(inflater, mFragmentView);
return mFragmentView;
}
@Override
public void onSaveInstanceState(final Bundle outState) {
if (isAdded()) {
final View v = getListView().getChildAt(0);
final int top = (v == null) ? 0 : v.getTop();
outState.putInt(STATE_POSITION, getListView().getFirstVisiblePosition());
outState.putInt(STATE_TOP, top);
}
super.onSaveInstanceState(outState);
}
@Override
public void onStop() {
super.onStop();
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "onStop");
// mCurrentState = null;
}
protected void bindView(final View view, final Bundle savedInstanceState) {
}
private void setupListView(final LayoutInflater inflater, final View view) {
final ListView listView = (ListView) mFragmentView.findViewById(android.R.id.list);
listView.setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(final AbsListView view, final int scrollState) {
}
@Override
public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount,
final int totalItemCount) {
triggerOnScrollListeners(listView, firstVisibleItem, visibleItemCount, totalItemCount);
}
});
if (listView instanceof PinnedHeaderListView) {
final PinnedHeaderListView pinnedListView = (PinnedHeaderListView) listView;
pinnedListView.setPinnedHeaderView(inflater.inflate(mLayoutListHeaderId, pinnedListView, false));
addOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(final AbsListView view, final int scrollState) {
}
@Override
public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount,
final int totalItemCount) {
final Adapter adapter = getListAdapter();
if (adapter != null && adapter instanceof OnScrollListener) {
final OnScrollListener sectionAdapter = (OnScrollListener) adapter;
sectionAdapter.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
});
}
}
public void loadContent() {
loadContent((Bundle) null);
}
public void loadContent(final Bundle bundle) {
if (getListAdapter() == null) {
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "loadContent " + getListAdapter());
getLoaderManager().initLoader(LOADER_INIT, bundle, this);
}
}
public void refreshContent() {
refreshContent((Bundle) null);
}
public void refreshContent(final Bundle bundle) {
getLoaderManager().restartLoader(LOADER_REFRESH, bundle, this);
}
protected void addOnScrollListener(final OnScrollListener onScrollListener) {
mOnScrollListeners.add(onScrollListener);
}
private void triggerOnScrollListeners(final AbsListView view, final int firstVisibleItem,
final int visibleItemCount, final int totalItemCount) {
for (final OnScrollListener l : mOnScrollListeners) {
l.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
/**
* Définir les textes et images affichés si la liste est vide.
*
* @param titleId
* L'identifiant du titre.
* @param summaryId
* L'identifiant de la description.
* @param drawableId
* L'identifiant du drawable.
*/
protected void setEmptyMessageValues(final int titleId, final int summaryId, final int drawableId) {
this.mMessageEmptyTitleId = titleId;
this.mMessageEmptySummaryId = summaryId;
this.mMessageEmptyDrawableId = drawableId;
}
/**
* Afficher l'indicateur de chargement.
*/
protected void showLoader() {
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "showLoader");
if (State.LOADER == mCurrentState) {
if (DBG)
Log.e(LOG_TAG + "$" + getClass().getSimpleName(), "\t showLoader NO");
return;
}
if (DBG)
Log.i(LOG_TAG + "$" + getClass().getSimpleName(), "\t showLoader OK");
mCurrentState = State.LOADER;
mFragmentView.findViewById(android.R.id.list).setVisibility(View.GONE);
if (mFragmentView.findViewById(R.id.fragmentMessage) != null) {
mFragmentView.findViewById(R.id.fragmentMessage).setVisibility(View.GONE);
}
mFragmentView.findViewById(R.id.fragmentLoading).setVisibility(View.VISIBLE);
}
/**
* Afficher le contenu.
*/
protected void showContent() {
if (State.CONTENT == mCurrentState) {
return;
}
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "showContent");
mCurrentState = State.CONTENT;
mFragmentView.findViewById(R.id.fragmentLoading).setVisibility(View.GONE);
if (mFragmentView.findViewById(R.id.fragmentMessage) != null) {
mFragmentView.findViewById(R.id.fragmentMessage).setVisibility(View.GONE);
}
final View content = mFragmentView.findViewById(android.R.id.list);
if (content.getVisibility() != View.VISIBLE) {
content.setVisibility(View.VISIBLE);
content.startAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in));
}
// View message = mFragmentView.findViewById(R.id.fragmentMessage);
// if (message != null) {
// message.setVisibility(View.GONE);
// }
}
/**
* Afficher le message avec un symbole d'erreur.
*
* @param titleRes
* L'identifiant du titre.
* @param descriptionRes
* L'identifiant de la description.
*/
protected void showError(final int titleRes, final int descriptionRes) {
showMessage(getString(titleRes), getString(descriptionRes), R.drawable.warning);
}
/**
* Afficher le message avec un symbole d'erreur.
*
* @param title
* Le titre.
* @param description
* La description.
*/
protected void showError(final String title, final String description) {
showMessage(title, description, R.drawable.warning);
}
/**
* Afficher le message.
*
* @param titleRes
* L'identifiant du titre.
* @param descriptionRes
* L'identifiant de la description.
* @param drawableRes
* L'identifiant du drawable.
*/
protected void showMessage(final int titleRes, final int descriptionRes, final int drawableRes) {
showMessage(getString(titleRes), (descriptionRes != 0) ? getString(descriptionRes) : null, drawableRes);
}
/**
* Afficher un message avec une desciption et un symbole.
*
* @param title
* Le titre.
* @param description
* La description.
* @param drawableRes
* L'identifiant du symbole.
*/
protected void showMessage(final String title, final String description, final int drawableRes) {
if (State.MESSAGE == mCurrentState) {
return;
}
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "showMessage " + title + "\t" + description + "\t"
+ drawableRes);
mCurrentState = State.MESSAGE;
mFragmentView.findViewById(android.R.id.list).setVisibility(View.GONE);
mFragmentView.findViewById(R.id.fragmentLoading).setVisibility(View.GONE);
View message = mFragmentView.findViewById(R.id.fragmentMessage);
if (message == null) {
final ViewStub messageStrub = (ViewStub) mFragmentView.findViewById(R.id.fragmentMessageStub);
message = messageStrub.inflate();
}
message.setVisibility(View.VISIBLE);
final TextView titleView = (TextView) message.findViewById(android.R.id.title);
titleView.setText(title);
titleView.setCompoundDrawablesWithIntrinsicBounds(0, drawableRes, 0, 0);
final TextView descriptionView = (TextView) message.findViewById(android.R.id.summary);
if (description != null) {
descriptionView.setText(description);
descriptionView.setVisibility(View.VISIBLE);
} else {
descriptionView.setVisibility(View.GONE);
}
}
/**
* Définir l'action du bouton lors de l'affichage du message.
*
* @param title
* Le titre du boutton.
* @param onClickListener
* Son action.
*/
protected void setMessageButton(final int title, final OnClickListener onClickListener) {
setMessageButton(getString(title), onClickListener);
}
/**
* Définir l'action du bouton lors de l'affichage du message.
*
* @param title
* Le titre du boutton.
* @param onClickListener
* Son action.
*/
protected void setMessageButton(final String title, final OnClickListener onClickListener) {
final View message = mFragmentView.findViewById(R.id.fragmentMessage);
if (message != null) {
final Button button = (Button) message.findViewById(android.R.id.button1);
button.setText(title);
button.setOnClickListener(onClickListener);
button.setVisibility(View.VISIBLE);
}
}
/**
* Définir le nombre de minutes pendant lesquelles les données sont
* considérées comme à jour
*
* @param timeToLive
*/
protected void setTimeToLive(final int timeToLive) {
this.mTimeToLive = timeToLive;
}
/**
* Redéfinir la date d'expiration du cache à maintenant
*/
protected void resetNextUpdate() {
mNextUpdate = new DateTime().plusMinutes(mTimeToLive);
}
/**
* Indique si les données sont toujours considérées comme à jour ou non
*
* @return true si elle ne sont plus à jour | false si elle sont à jour
*/
protected boolean isNotUpToDate() {
if (mNextUpdate != null) {
return (mNextUpdate.isBeforeNow());
} else {
return true;
}
}
/**
* Avant le chargement.
*/
protected void onPreExecute() {
}
/**
* Charger le contenu en background.
*
* @return AsyncResult du resultat.
*/
protected abstract AsyncResult<ListAdapter> loadContent(final Context context, final Bundle bundle);
/**
* Après le chargement.
*/
protected void onPostExecute() {
}
@Override
public Loader<AsyncResult<ListAdapter>> onCreateLoader(final int loaderId, final Bundle bundle) {
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "onCreateLoader");
final Loader<AsyncResult<ListAdapter>> loader = new AsyncTaskLoader<AsyncResult<ListAdapter>>(getActivity()) {
private AsyncResult<ListAdapter> mResult;
@Override
public AsyncResult<ListAdapter> loadInBackground() {
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "loadInBackground");
return loadContent(getActivity(), bundle);
}
/**
* Called when there is new data to deliver to the client. The super
* class will take care of delivering it; the implementation here
* just adds a little more logic.
*/
@Override
public void deliverResult(final AsyncResult<ListAdapter> result) {
mResult = result;
if (isStarted()) {
// If the Loader is currently started, we can immediately
// deliver its results.
try {
super.deliverResult(result);
} catch (final NullPointerException e) {
}
}
}
/**
* Handles a request to start the Loader.
*/
@Override
protected void onStartLoading() {
if (mResult != null) {
// If we currently have a result available, deliver it
// immediately.
deliverResult(mResult);
}
if (takeContentChanged() || mResult == null) {
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
forceLoad();
}
}
};
if (getListAdapter() == null || getListAdapter().getCount() == 0)
showLoader();
onPreExecute();
return loader;
}
@Override
public void onLoadFinished(final Loader<AsyncResult<ListAdapter>> loader, final AsyncResult<ListAdapter> result) {
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "onLoadFinished " + result);
if (result == null) {
showMessage(mMessageEmptyTitleId, mMessageEmptySummaryId, mMessageEmptyDrawableId);
return;
}
final Exception exception = result.getException();
if (exception == null) {
final ListAdapter adapter = result.getResult();
setListAdapter(adapter);
if (adapter == null) {
showMessage(mMessageEmptyTitleId, mMessageEmptySummaryId, mMessageEmptyDrawableId);
} else {
adapter.registerDataSetObserver(new DataSetObserver() {
@Override
public void onChanged() {
super.onChanged();
onListAdapterChange(adapter);
}
});
if (adapter.getCount() > 0) {
if (mListViewStatePosition != -1 && isAdded()) {
getListView().setSelectionFromTop(mListViewStatePosition, mListViewStateTop);
mListViewStatePosition = -1;
}
showContent();
resetNextUpdate();
} else {
showMessage(mMessageEmptyTitleId, mMessageEmptySummaryId, mMessageEmptyDrawableId);
}
}
} else {
int titleRes = R.string.error_title;
int messageRes = R.string.error_summary;
int drawableRes = R.drawable.warning;
// Erreur réseau ou interne ?
if (exception instanceof IOException) {
titleRes = R.string.error_title_network;
messageRes = R.string.error_summary_network;
drawableRes = R.drawable.ic_thunderstorm;
} else if (exception instanceof JSONException) {
titleRes = R.string.error_title_webservice;
messageRes = R.string.error_summary_webservice;
}
if (getListAdapter() == null || getListAdapter().isEmpty()) {
showMessage(titleRes, messageRes, drawableRes);
} else {
Crouton.makeText(getActivity(), titleRes, Style.ALERT, (ViewGroup) getView()).show();
}
Log.e(getClass().getSimpleName(), "Erreur de chargement.", exception);
}
onPostExecute();
}
@Override
public void onLoaderReset(final Loader<AsyncResult<ListAdapter>> arg0) {
}
@Override
public void setListAdapter(final ListAdapter adapter) {
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "setListAdapter " + adapter);
super.setListAdapter(adapter);
}
/**
* Gestion du changement du contenu de l'adapter : affichage ou non du
* message comme quoi la liste est vide.
*
* @param adapter
*/
public void onListAdapterChange(final ListAdapter adapter) {
if (DBG)
Log.d(LOG_TAG + "$" + getClass().getSimpleName(), "onListAdapterChange");
if (adapter == null || adapter.getCount() == 0) {
showMessage(mMessageEmptyTitleId, mMessageEmptySummaryId, mMessageEmptyDrawableId);
} else {
showContent();
}
}
}