/*
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.ACTION_BADGE_EARNED;
import static com.concentricsky.android.khanacademy.Constants.ACTION_LIBRARY_UPDATE;
import static com.concentricsky.android.khanacademy.Constants.ACTION_TOAST;
import static com.concentricsky.android.khanacademy.Constants.EXTRA_BADGE;
import static com.concentricsky.android.khanacademy.Constants.EXTRA_MESSAGE;
import static com.concentricsky.android.khanacademy.Constants.PARAM_TOPIC_ID;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import android.app.ActionBar;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.widget.CursorAdapter;
import android.support.v4.widget.SimpleCursorAdapter;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.TextView;
import android.widget.Toast;
import com.concentricsky.android.khan.R;
import com.concentricsky.android.khanacademy.Constants.Direction;
import com.concentricsky.android.khanacademy.MainMenuDelegate;
import com.concentricsky.android.khanacademy.data.KADataService;
import com.concentricsky.android.khanacademy.data.db.Badge;
import com.concentricsky.android.khanacademy.data.db.Thumbnail;
import com.concentricsky.android.khanacademy.data.db.Topic;
import com.concentricsky.android.khanacademy.data.db.User;
import com.concentricsky.android.khanacademy.util.Log;
import com.concentricsky.android.khanacademy.util.ObjectCallback;
import com.concentricsky.android.khanacademy.util.ThumbnailManager;
import com.concentricsky.android.khanacademy.views.ThumbnailViewRenderer;
import com.concentricsky.android.khanacademy.views.ThumbnailViewRenderer.Param;
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 class TopicListActivity extends KADataServiceProviderActivityBase {
public static final String LOG_TAG = TopicListActivity.class.getSimpleName();
private GridView gridView;
private View headerView;
private Topic topic; // Use this instead of topicId so we can hang onto `topic.child_kind' as well.
private String topicId;
private Dao<Topic, String> dao;
private ThumbnailManager thumbnailManager;
private MainMenuDelegate mainMenuDelegate;
private Menu mainMenu;
private KADataService dataService;
private Cursor topicCursor;
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_LIBRARY_UPDATE.equals(intent.getAction()) && topic != null) {
Log.d(LOG_TAG, "library update broadcast received");
TopicGridAdapter adapter = getUnwrappedAdapter();
if (adapter != null) {
if (topicCursor != null) {
topicCursor.close();
}
topicCursor = buildCursor(topicId);
adapter.changeCursor(topicCursor);
}
} else if (ACTION_BADGE_EARNED.equals(intent.getAction()) && dataService != null) {
Badge badge = (Badge) intent.getSerializableExtra(EXTRA_BADGE);
dataService.getAPIAdapter().toastBadge(badge);
} else if (ACTION_TOAST.equals(intent.getAction())) {
Toast.makeText(TopicListActivity.this, intent.getStringExtra(EXTRA_MESSAGE), Toast.LENGTH_SHORT).show();
}
}
};
// Used to avoid touching the ui with AsyncTask callbacks after the ui is no longer available.
boolean stopped = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_topic_list);
// Set / restore state.
Intent intent = getIntent();
topicId = intent != null && intent.hasExtra(PARAM_TOPIC_ID)
? intent.getStringExtra(PARAM_TOPIC_ID)
: savedInstanceState != null && savedInstanceState.containsKey(PARAM_TOPIC_ID)
? savedInstanceState.getString(PARAM_TOPIC_ID)
: null;
}
@Override
protected void onStart() {
super.onStart();
stopped = false;
mainMenuDelegate = new MainMenuDelegate(this);
gridView = (GridView) findViewById(R.id.activity_topic_list_grid);
gridView.setOnItemClickListener(clickListener);
ActionBar ab = getActionBar();
ab.setDisplayHomeAsUpEnabled(true);
ab.setTitle("Topics");
requestDataService(new ObjectCallback<KADataService>() {
@Override
public void call(final KADataService dataService) {
TopicListActivity.this.dataService = dataService;
try {
thumbnailManager = dataService.getThumbnailManager();
dao = dataService.getHelper().getTopicDao();
if (topicId != null) {
topic = dao.queryForId(topicId);
}
else {
topic = dataService.getRootTopic();
topicId = topic.getId();
}
// DEBUG
if (topic == null) return;
// header
headerView = findViewById(R.id.header_topic_list);
((TextView) headerView.findViewById(R.id.header_video_list_title)).setText(topic.getTitle());
String desc = topic.getDescription();
TextView descView = (TextView) headerView.findViewById(R.id.header_video_list_description);
if (desc != null && desc.length() > 0) {
descView.setText(Html.fromHtml(desc));
descView.setVisibility(View.VISIBLE);
} else {
descView.setVisibility(View.GONE);
}
// Find child count for this parent topic.
boolean videoChildren = Topic.CHILD_KIND_VIDEO.equals(topic.getChild_kind());
String[] idArg = {topic.getId()};
String sql = videoChildren ? "select count() from topicvideo where topic_id=?"
: "select count() from topic where parentTopic_id=?";
int count = (int) dao.queryRawValue(sql, idArg);
String countFormat = getString(videoChildren ? R.string.format_video_count : R.string.format_topic_count);
// Set header count string.
((TextView) headerView.findViewById(R.id.header_video_list_count)).setText(
String.format(countFormat, count));
final ImageView thumb = (ImageView) headerView.findViewById(R.id.header_video_list_thumbnail);
if (thumb != null) {
new AsyncTask<Void, Void, Bitmap>() {
@Override public Bitmap doInBackground(Void... arg) {
Bitmap bmp = thumbnailManager.getThumbnail(TopicListActivity.this.topic.getThumb_id(), Thumbnail.QUALITY_SD);
return bmp;
}
@Override public void onPostExecute(Bitmap bmp) {
thumb.setImageBitmap(bmp);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
// list
if (topicCursor != null) {
topicCursor.close();
}
topicCursor = buildCursor(topicId);
ListAdapter adapter = new TopicGridAdapter(topicCursor);
gridView.setAdapter(adapter);
// Request the topic and its children be updated from the api.
List<Topic> children = dao.queryForEq("parentTopic_id", topic.getId());
List<String> toUpdate = new ArrayList<String>();
for (Topic child : children) {
toUpdate.add(child.getId());
}
toUpdate.add(topic.getId());
} catch (SQLException e) {
e.printStackTrace();
}
}
});
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_LIBRARY_UPDATE);
filter.addAction(ACTION_BADGE_EARNED);
filter.addAction(ACTION_TOAST);
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter);
}
@Override
public void onStop() {
Log.d(LOG_TAG, "onStop");
stopped = true;
if (gridView != null) {
gridView.setOnItemClickListener(null);
TopicGridAdapter adapter = (TopicGridAdapter) gridView.getAdapter();
if (adapter != null) {
adapter.changeCursor(null);
adapter.renderer.stop();
adapter.renderer.clearCache();
}
gridView.setAdapter(null);
gridView = null;
}
if (headerView != null) {
final ImageView thumb = (ImageView) headerView.findViewById(R.id.header_video_list_thumbnail);
// thumb.setImageResource(0);
Drawable d = thumb.getDrawable();
if (d instanceof BitmapDrawable) {
Bitmap bmp = ((BitmapDrawable) d).getBitmap();
if (bmp != null) {
bmp.recycle();
}
}
}
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
receiver = null;
super.onStop();
}
private TopicGridAdapter getUnwrappedAdapter() {
if (gridView != null) {
ListAdapter adapter = gridView.getAdapter();
if (adapter instanceof TopicGridAdapter) {
return (TopicGridAdapter) adapter;
}
}
return null;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
mainMenu = menu;
mainMenuDelegate.onCreateOptionsMenu(menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
requestDataService(new ObjectCallback<KADataService>() {
@Override
public void call(KADataService dataService) {
User user = dataService.getAPIAdapter().getCurrentUser();
boolean show = user != null;
mainMenu.findItem(R.id.menu_logout).setEnabled(show).setVisible(show);
}
});
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (mainMenuDelegate.onOptionsItemSelected(item)) {
return true;
}
switch (item.getItemId()) {
case R.id.menu_logout:
requestDataService(new ObjectCallback<KADataService>() {
@Override
public void call(KADataService dataService) {
dataService.getAPIAdapter().logout();
}
});
return true;
case android.R.id.home:
// TopicList for this topic's parent topic, or home in case of "root".
if (topic == null) {
// Not sure what to do.
launchHomeActivity();
}
else {
Topic parentTopic = topic.getParentTopic();
try {
dataService.getHelper().getTopicDao().refresh(parentTopic);
} catch (SQLException e) {
e.printStackTrace();
}
if (parentTopic == null) {
// This is the root topic. How did that happen?
launchHomeActivity();
}
else {
if (parentTopic.getParentTopic() == null) {
// The parent is the root topic.
launchHomeActivity();
}
else {
launchListActivity(parentTopic.getId(), TopicListActivity.class, Direction.BACKWARD);
}
}
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void launchHomeActivity() {
Intent intent = new Intent(this, HomeActivity.class);
// ALWAYS clear top when going home.
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
private void launchListActivity(String topicId, Class<?> activityClass, Direction direction) {
Intent intent = new Intent(this, activityClass);
intent.putExtra(PARAM_TOPIC_ID, topicId);
switch (direction) {
case BACKWARD:
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
finish();
default: // nop
}
startActivity(intent);
}
private AdapterView.OnItemClickListener clickListener = new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> gridView, View clickedView, int position, long id) {
Log.d(LOG_TAG, "onItemClick");
// TODO : Header clicks.
// Get the clicked item (a properly positioned cursor).
Cursor item = (Cursor) gridView.getAdapter().getItem(position);
String topicId = item.getString(item.getColumnIndex("_id"));
String kind = item.getString(item.getColumnIndex("child_kind"));
Log.d(LOG_TAG, String.format(" child_kind is %s", kind));
// Should only ever find CHILD_KIND_TOPIC or CHILD_KIND_VIDEO here, thanks to the query used (see `buildCursor' below).
Class<?> activityClass = Topic.CHILD_KIND_TOPIC.equals(kind)
? TopicListActivity.class
: VideoListActivity.class;
launchListActivity(topicId, activityClass, Direction.FORWARD);
}
};
private class Renderer extends ThumbnailViewRenderer {
private CursorAdapter mAdapter;
private int titleColumn, childKindColumn, idColumn;
private boolean prepared = false;
public Renderer(CursorAdapter adapter, ThumbnailManager thumbnailManager, int cacheCapacity) {
super(2, R.id.pane_topic_image, thumbnailManager, Thumbnail.QUALITY_SD, cacheCapacity);
mAdapter = adapter;
}
@Override
protected void prepare(View view, Param param, int immediatePassHint) {
super.prepare(view, param, immediatePassHint);
TextView titleView = (TextView) view.findViewById(R.id.pane_topic_title);
TextView countView = (TextView) view.findViewById(R.id.pane_topic_video_count);
TextView descView = (TextView) view.findViewById(R.id.pane_topic_description);
Cursor c = (Cursor) mAdapter.getItem(param.cursorPosition);
if (!prepared) {
titleColumn = c.getColumnIndex("title");
childKindColumn = c.getColumnIndex("child_kind");
idColumn = c.getColumnIndex("_id");
prepared = true;
}
String title = c.getString(titleColumn);
boolean videoChildren = Topic.CHILD_KIND_VIDEO.equals(c.getString(childKindColumn));
String countFormat = getString(videoChildren ? R.string.format_video_count : R.string.format_topic_count);
String[] idArg = {c.getString(idColumn)};
String sql = videoChildren ? "select count() from topicvideo where topic_id=?"
: "select count() from topic where parentTopic_id=?";
int count = 0;
try {
// TODO : Denormalize the child count so we don't need to query here.
count = (int) dao.queryRawValue(sql, idArg);
} catch (SQLException e) {
e.printStackTrace();
}
String countString = String.format(countFormat, count);
descView.setVisibility(View.GONE);
titleView.setText(title);
countView.setText(countString);
}
}
private class TopicGridAdapter extends SimpleCursorAdapter {
private final Renderer renderer;
TopicGridAdapter(Cursor cursor) {
super(TopicListActivity.this, R.layout.pane_topic, cursor,
new String[] {"title"}, new int[] {R.id.pane_topic_title}, 0);
Runtime rt = Runtime.getRuntime();
long maxMemory = rt.maxMemory();
// Want to use at most about 1/2 of available memory for thumbs.
long usableMemory = maxMemory / 2;
// Higher dpi devices use more memory for other things, so we will have a smaller thumb cache.
// Fire HD 7 is 216dpi, 8.9 is 254, transformer is 150, majority of devices <= 256, occasional ~326, one outlier at 440.
// On transformer, a cache size of maxMemory / 2 was comfortable, so for now we'll try scaling from there.
// This yields a max count of about 13 thumbs on Fire HD 7, 20 on transformer.
usableMemory /= getResources().getDisplayMetrics().density;
int thumbSize = 640 * 480 * 4; // QUALITY_SD at 4 bytes per pixel
int maxCachedCount = (int) (usableMemory / thumbSize);
renderer = new Renderer(this, thumbnailManager, maxCachedCount);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view == null) {
LayoutInflater inflater = LayoutInflater.from(context);
view = inflater.inflate(R.layout.pane_topic, null, false);
}
renderer.renderView(view, new Param(cursor.getPosition(), cursor.getString(cursor.getColumnIndex("thumb_id"))));
}
}
private Cursor buildCursor(String topicId) {
AndroidDatabaseResults iterator = null;
try {
// select count() from topic, video where video.parentTopic_id=topic._id and video.seq=0;
QueryBuilder<Topic, String> qb = this.dao.queryBuilder();
qb.orderBy("seq", true);
Where<Topic, String> where = qb.where();
where.eq("parentTopic_id", topicId).and().gt("video_count", 0).and().in("child_kind", Topic.CHILD_KIND_TOPIC, Topic.CHILD_KIND_VIDEO);
PreparedQuery<Topic> pq = qb.prepare();
iterator = (AndroidDatabaseResults) dao.iterator(pq).getRawResults();
return iterator.getRawCursor();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}