/* Viewer for Khan Academy Copyright (C) 2012 Concentric Sky, Inc. This program 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. This program 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 com.concentricsky.android.khanacademy.app; import static com.concentricsky.android.khanacademy.Constants.PARAM_TOPIC_ID; import java.sql.SQLException; import android.app.Activity; import android.content.Context; import android.database.Cursor; import android.os.Bundle; import android.widget.CursorAdapter; import android.widget.ListAdapter; import android.widget.ListView; import com.concentricsky.android.khanacademy.Constants; import com.concentricsky.android.khanacademy.data.KADataService; import com.concentricsky.android.khanacademy.data.KADataService.ServiceUnavailableException; import com.concentricsky.android.khanacademy.data.db.DatabaseHelper; import com.concentricsky.android.khanacademy.data.db.EntityBase; import com.concentricsky.android.khanacademy.data.db.Topic; import com.concentricsky.android.khanacademy.util.Log; import com.concentricsky.android.khanacademy.util.ObjectCallback; import com.j256.ormlite.android.AndroidDatabaseResults; import com.j256.ormlite.dao.Dao; import com.j256.ormlite.stmt.PreparedQuery; import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.stmt.Where; public abstract class AbstractListFragment<T extends EntityBase> extends android.app.ListFragment implements ObjectCallback<KADataService> { public static final String LOG_TAG = AbstractListFragment.class.getSimpleName(); public static final int LIST_VIEW_ID = 123890; protected Dao<T, String> dao; private Callbacks callbacks; private String topicId; private boolean isShowingDownloadedVideosOnly; private Topic topic; private Cursor topicCursor; protected Callbacks getCallbacks() { return callbacks; } // ABSTRACT protected abstract ListAdapter buildListAdapter(); protected abstract Class<T> getEntityClass(); public interface Callbacks extends KADataService.Provider { public void onRefreshRequested(); } // CONSTRUCTORS public AbstractListFragment() { super(); } // LIFECYCLE /** * Build the bundle that will be passed to a future onCreate call. */ @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Log.v(LOG_TAG, ".onSaveInstanceState"); outState.putString(PARAM_TOPIC_ID, topicId); } /** * Load state from savedInstanceState / shared preferences. * * Cleanup in onDestroy. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.v(LOG_TAG, ".onCreate"); // Ensure that the Activity implements the correct Callbacks interface. @SuppressWarnings("rawtypes") Class<? extends AbstractListFragment> cls = getClass(); Activity a = getActivity(); boolean okay = false; for (Class<?> c : cls.getDeclaredClasses()) { if (Callbacks.class.isAssignableFrom(c) && c.isInstance(a)) { okay = true; } } if (!okay) { throw new IllegalStateException(String.format("Activity must implement %s.Callbacks", cls.getSimpleName())); } callbacks = (Callbacks) getActivity(); // Set / restore state. topicId = null; if (savedInstanceState != null && savedInstanceState.containsKey(PARAM_TOPIC_ID)) { String id = savedInstanceState.getString(PARAM_TOPIC_ID); if (id != null) { topicId = id; } } else { Bundle args = getArguments(); if (args != null && args.containsKey(PARAM_TOPIC_ID)) { String id = args.getString(PARAM_TOPIC_ID); if (id != null) { topicId = id; } } } isShowingDownloadedVideosOnly = getActivity().getSharedPreferences( Constants.SETTINGS_NAME, Context.MODE_PRIVATE).getBoolean(Constants.SETTING_SHOW_DL_ONLY, false); } /** * Counterpart to onCreate. */ @Override public void onDestroy() { Log.v(LOG_TAG, ".onDestroy"); callbacks = null; super.onDestroy(); } /** * Build adapter, attach to data service, get appropriate cursor, and reset the list. * * Cleanup in onDestroyView. */ @Override public void onActivityCreated(Bundle savedInstanceState) { Log.v(LOG_TAG, ".onActivityCreated"); super.onActivityCreated(savedInstanceState); ListAdapter adapter = buildListAdapter(); setListAdapter(adapter); Activity a = getActivity(); try { KADataService dataService = ((KADataService.Provider) a).getDataService(); this.call(dataService); Log.d(LOG_TAG, "Service already available."); } catch (ServiceUnavailableException e) { boolean serviceExpected = ((KADataService.Provider) a).requestDataService(this); Log.d(LOG_TAG, String.format("Service expected: %b.", serviceExpected)); } getListView().setOverScrollMode(ListView.OVER_SCROLL_ALWAYS); } /** * Counterpart to onCreateView. Since onActivityCreated has no counterpart, * do those things here also. */ @Override public void onDestroyView() { Log.d(LOG_TAG, ".onDestroyView"); if (topicCursor != null) { // This is opened in onActivityCreated. topicCursor.close(); } setListAdapter(null); ((KADataService.Provider) getActivity()).cancelDataServiceRequest(this); super.onDestroyView(); } // CALLBACKS /** * implements ObjectCallback<KADataService> */ @Override public void call(KADataService service) { // Called when the service becomes available. if (topicId != null) { DatabaseHelper dbh = service.getHelper(); try { Dao<Topic, String> topicDao = dbh.getTopicDao(); topic = topicDao.queryForId(topicId); dao = dbh.getDao(getEntityClass()); } catch (SQLException e) { e.printStackTrace(); } } else { topic = service.getRootTopic(); topicId = topic.getId(); } resetListContents(topicId); } // PRIVATE private void resetListContents(String topicId) { Log.d(LOG_TAG, "resetListContents"); AndroidDatabaseResults iterator = null; QueryBuilder<T, String> qb = this.dao.queryBuilder(); qb = qb.orderBy("seq", true); try { Where<T, String> where = qb.where(); where.eq("parentTopic_id", topicId); addToQuery(where); PreparedQuery<T> pq = qb.prepare(); iterator = (AndroidDatabaseResults) dao.iterator(pq).getRawResults(); if (topicCursor != null) { topicCursor.close(); } topicCursor = iterator.getRawCursor(); } catch (SQLException e) { e.printStackTrace(); } CursorAdapter adapter = (CursorAdapter) getListAdapter(); adapter.changeCursor(topicCursor); adapter.notifyDataSetChanged(); } // PUBLIC public void setActivateOnItemClick(boolean activateOnItemClick) { getListView().setChoiceMode(activateOnItemClick ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE); } public String getTitle() { // TODO : special case the root return topic.getTitle(); } public Topic getTopic() { return topic; } /** * Whether to show all videos, or just the downloaded ones. * * There is no setter for this, as a change will just cause a new fragment to be created. * * @return true if the fragment should display only the videos with local copies, false otherwise. */ protected boolean isShowingDownloadedVideosOnly() { return isShowingDownloadedVideosOnly; } /** * Make any needed modifications to the query just before it is executed and its cursor passed to the list adapter. * * Default implementation does nothing; subclasses override to customize. * * @param query The query to modify. * @return The same query, after modifying it. * @throws SQLException */ protected Where<T, String> addToQuery(Where<T, String> where) throws SQLException { return where; } }