com.concentricsky.android.khanacademy.app.VideoListActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.concentricsky.android.khanacademy.app.VideoListActivity.java

Source

/*
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_DOWNLOAD_PROGRESS_UPDATE;
import static com.concentricsky.android.khanacademy.Constants.ACTION_LIBRARY_UPDATE;
import static com.concentricsky.android.khanacademy.Constants.ACTION_OFFLINE_VIDEO_SET_CHANGED;
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.EXTRA_STATUS;
import static com.concentricsky.android.khanacademy.Constants.PARAM_SHOW_DL_ONLY;
import static com.concentricsky.android.khanacademy.Constants.PARAM_TOPIC_ID;
import static com.concentricsky.android.khanacademy.Constants.PARAM_VIDEO_ID;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.widget.CursorAdapter;
import android.text.Html;
import android.text.method.ScrollingMovementMethod;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.HeaderViewListAdapter;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.SpinnerAdapter;
import android.widget.TextView;
import android.widget.Toast;

import com.concentricsky.android.khan.R;
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.data.db.Video;
import com.concentricsky.android.khanacademy.data.remote.KAAPIAdapter;
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.dao.Dao;
import com.j256.ormlite.stmt.Where;

public class VideoListActivity extends KADataServiceProviderActivityBase {

    public static final String LOG_TAG = VideoListActivity.class.getSimpleName();
    private static final int DOWNLOAD_ITEM_ID = 1543413;

    private String topicId;
    private Topic topic;
    private View headerView;
    private AbsListView listView;
    private ThumbnailManager thumbnailManager;
    private KAAPIAdapter api;
    private boolean isShowingDownloadedVideosOnly;
    private String[] displayOptions = new String[] { "All Videos", "Downloaded Videos" };
    private SpinnerAdapter displayOptionsAdapter;
    private MainMenuDelegate mainMenuDelegate;
    private Menu mainMenu;
    private KADataService dataService;
    private Cursor topicCursor;
    private ExecutorService thumbExecutor;

    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");
                setParentTopic(topic);
            } else if (ACTION_BADGE_EARNED.equals(intent.getAction()) && dataService != null) {
                Badge badge = (Badge) intent.getSerializableExtra(EXTRA_BADGE);
                dataService.getAPIAdapter().toastBadge(badge);
            } else if (ACTION_DOWNLOAD_PROGRESS_UPDATE.equals(intent.getAction()) && listView != null) {
                @SuppressWarnings("unchecked")
                Map<String, Integer> status = (Map<String, Integer>) intent.getSerializableExtra(EXTRA_STATUS);
                VideoAdapter adapter = (VideoAdapter) listView.getAdapter();
                adapter.setStatus(status);
                adapter.updateBars();
            } else if (ACTION_OFFLINE_VIDEO_SET_CHANGED.equals(intent.getAction()) && listView != null) {
                resetListContents(topicId);
            } else if (ACTION_TOAST.equals(intent.getAction())) {
                Toast.makeText(VideoListActivity.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;

    private ActionBar.OnNavigationListener navListener = new ActionBar.OnNavigationListener() {
        @Override
        public boolean onNavigationItemSelected(int itemPosition, long itemId) {
            Log.d(LOG_TAG, "onNavigationItemSelected: " + itemPosition);

            isShowingDownloadedVideosOnly = itemPosition == 1;
            if (topic != null) {
                setParentTopic(topic);
            }

            return true;
        }
    };
    private KAAPIAdapter.UserUpdateListener userUpdateListener = new KAAPIAdapter.UserUpdateListener() {
        @Override
        public void onUserUpdate(User user) {
            resetListContents(topicId);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_video_list);

        Intent intent = getIntent();
        topicId = savedInstanceState != null && savedInstanceState.containsKey(PARAM_TOPIC_ID)
                ? savedInstanceState.getString(PARAM_TOPIC_ID)
                : intent != null && intent.hasExtra(PARAM_TOPIC_ID) ? intent.getStringExtra(PARAM_TOPIC_ID) : null;

        isShowingDownloadedVideosOnly = savedInstanceState != null
                && savedInstanceState.containsKey(PARAM_SHOW_DL_ONLY)
                        ? savedInstanceState.getBoolean(PARAM_SHOW_DL_ONLY)
                        : intent != null && intent.hasExtra(PARAM_SHOW_DL_ONLY)
                                ? intent.getBooleanExtra(PARAM_SHOW_DL_ONLY, false)
                                : false;
    }

    @Override
    protected void onStart() {
        Log.d(LOG_TAG, "onStart");
        super.onStart();
        stopped = false;

        mainMenuDelegate = new MainMenuDelegate(this);

        listView = (AbsListView) findViewById(android.R.id.list);
        listView.setOnItemClickListener(clickListener);

        if (listView instanceof ListView) {
            // It is important that this is inflated with listView passed as the parent, despite the attach false parameter.
            // Otherwise, the view ends up with incorrect LayoutParams and we see crazy, crazy behavior.
            headerView = getLayoutInflater().inflate(R.layout.header_video_list, listView, false);
            ListView lv = (ListView) listView;
            if (lv.getHeaderViewsCount() == 0) {
                lv.addHeaderView(headerView);
            }
        } else { // GridView, fixed header
            headerView = findViewById(R.id.header_video_list);
        }

        /**  Responsive layout stuff 
         * 
         *  Based on screen width, we will find either
         *   narrow
         *     a listview with a header view
         *     items are a thumbnail to the left and a title on white space to the right.
         *     header is a thumbnail with overlaid title across the bottom
         * 
         *   middle
         *     a fixed header on top and a grid view below
         *     header is thumb to left, title above scrolling description to right
         *     items are thumbs with title overlaid across the bottom (3 across)
         *   
         *   wide
         *     a fixed header to the left and a grid view on the right
         *     header is thumb on top, title next, description at bottom
         *     items are thumbs with title overlaid across the bottom (3 across)
         *  
         *  
         *  So in this class, we 
         *    find view by id 'list'
         *    if it's a ListView, inflate and attach header view
         *    if not, then the header is fixed and already in the layout
         *    either way, now we can find header views by id
         *    adapter is the same either way
         *  
         *  
         *  
         *  **/

        ActionBar ab = getActionBar();
        displayOptionsAdapter = new ArrayAdapter<String>(getActionBar().getThemedContext(),
                android.R.layout.simple_list_item_1, displayOptions);
        ab.setDisplayHomeAsUpEnabled(true);
        ab.setTitle("");
        ab.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
        ab.setListNavigationCallbacks(displayOptionsAdapter, navListener);
        ab.setSelectedNavigationItem(isShowingDownloadedVideosOnly ? 1 : 0);

        requestDataService(new ObjectCallback<KADataService>() {
            @Override
            public void call(KADataService dataService) {
                VideoListActivity.this.dataService = dataService;

                if (topicId != null) {
                    Dao<Topic, String> topicDao;
                    try {
                        topicDao = dataService.getHelper().getTopicDao();
                        topic = topicDao.queryForId(topicId);
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                } else {
                    Log.e(LOG_TAG, "Topic id not set for video list");
                    topic = dataService.getRootTopic();
                    topicId = topic.getId();
                }

                thumbnailManager = dataService.getThumbnailManager();
                api = dataService.getAPIAdapter();
                api.registerUserUpdateListener(userUpdateListener);

                // This instead happens in ActionBar.OnNavigationListener#onNavigationItemSelected, which
                // fires after onResume.
                //            setParentTopic(topic);
            }
        });

        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_LIBRARY_UPDATE);
        filter.addAction(ACTION_BADGE_EARNED);
        filter.addAction(ACTION_OFFLINE_VIDEO_SET_CHANGED);
        filter.addAction(ACTION_DOWNLOAD_PROGRESS_UPDATE);
        filter.addAction(ACTION_TOAST);
        LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter);
        thumbExecutor = Executors.newSingleThreadExecutor();
    }

    @Override
    protected void onStop() {
        Log.d(LOG_TAG, "onStop");
        stopped = true;

        getActionBar().setListNavigationCallbacks(null, null);
        if (listView != null) {
            // Could probably go through and cancel all ThumbLoaders here.
            VideoAdapter adapter = (VideoAdapter) listView.getAdapter();
            if (adapter != null) {
                adapter.renderer.stop();
                adapter.renderer.clearCache();
                adapter.changeCursor(null);
            }
            listView.setAdapter(null);
            listView.setOnItemClickListener(null);
            listView = null;
        }
        if (api != null) {
            api.unregisterUserUpdateListener(userUpdateListener);
        }
        mainMenuDelegate = null;
        LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
        thumbExecutor.shutdownNow();
        super.onStop();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        mainMenu = menu;
        mainMenuDelegate.onCreateOptionsMenu(menu);

        MenuItem downloadItem = mainMenu.add(0, DOWNLOAD_ITEM_ID, mainMenu.size(), R.string.menu_item_download_all);
        downloadItem.setIcon(R.drawable.av_download);
        downloadItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);

        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);
                    }
                }
            }
            return true;
        case DOWNLOAD_ITEM_ID:
            confirmAndDownloadAll();
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    private void confirmAndDownloadAll() {
        Dao<Video, String> videoDao;
        try {
            videoDao = dataService.getHelper().getVideoDao();
            // TODO : Instead, startService with a topicId and let the service background the lookup.
            //        This would take some callback juggling, though, as we want the count available for the dialog.
            final List<Video> toDownload = videoDao.queryRaw(
                    "select video.* from video, topicvideo where topicvideo.video_id=video.readable_id and topicvideo.topic_id=? and video.download_status<?",
                    videoDao.getRawRowMapper(), topicId, String.valueOf(Video.DL_STATUS_COMPLETE)).getResults();

            String msg = "";
            int size = toDownload.size();
            switch (size) {
            case 0:
            case 1:
                dataService.getOfflineVideoManager().downloadAll(toDownload);
                return;
            case 2:
                msg = getString(R.string.msg_download_both);
                break;
            default:
                msg = String.format(getString(R.string.msg_download_all), toDownload.size());
            }

            new AlertDialog.Builder(VideoListActivity.this).setMessage(msg)
                    .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int id) {
                            dataService.getOfflineVideoManager().downloadAll(toDownload);
                        }
                    }).setNegativeButton(android.R.string.no, null).show();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private AdapterView.OnItemClickListener clickListener = new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            Cursor cursor = (Cursor) listView.getItemAtPosition(position);
            if (cursor != null) { // This happened once on Fire HD when clicking outside the list, just as activity opened.
                String readableId = cursor.getString(cursor.getColumnIndex("readable_id"));
                launchVideoDetailActivity(readableId, topicId);
            }
        }
    };

    private void launchVideoDetailActivity(String videoId, String parentTopicId) {
        Intent intent = new Intent(this, VideoDetailActivity.class);
        intent.putExtra(PARAM_VIDEO_ID, videoId);
        if (parentTopicId != null) {
            intent.putExtra(PARAM_TOPIC_ID, parentTopicId);
        }
        startActivity(intent);
    }

    private void launchListActivity(String topicId, Class<?> activityClass) {
        Intent intent = new Intent(this, activityClass);
        intent.putExtra(PARAM_TOPIC_ID, topicId);
        intent.putExtra(PARAM_SHOW_DL_ONLY, isShowingDownloadedVideosOnly());
        // ALWAYS goes to this topic's parent topic. If this assumption breaks, then we must rethink the clear_top flag.
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        startActivity(intent);
    }

    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 VideoAdapter getUnwrappedAdapter() {
        if (listView != null) {
            ListAdapter adapter = listView.getAdapter();
            if (adapter instanceof HeaderViewListAdapter) {
                adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter();
            }
            if (adapter instanceof VideoAdapter) {
                return (VideoAdapter) adapter;
            }
        }

        return null;
    }

    private void setParentTopic(Topic topic) {
        this.topic = topic;

        if (topic != null) {
            topicId = topic.getId();

            // header
            ((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).toString());
                descView.setVisibility(View.VISIBLE);
                descView.setMovementMethod(new ScrollingMovementMethod());
            } else {
                descView.setVisibility(View.GONE);
            }

            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(VideoListActivity.this.topic.getThumb_id(),
                                Thumbnail.QUALITY_SD);
                        return bmp;
                    }

                    @Override
                    public void onPostExecute(Bitmap bmp) {
                        thumb.setImageBitmap(bmp);
                    }
                }.execute();
            }

            String countFormat;
            int param;
            if (isShowingDownloadedVideosOnly()) {
                countFormat = getString(R.string.format_downloaded_count);
                param = dataService.getOfflineVideoManager().getDownloadCountForTopic(dataService.getHelper(),
                        topicId, 1);
            } else {
                countFormat = getString(R.string.format_video_count);
                param = topic.getVideo_count();
            }
            ((TextView) headerView.findViewById(R.id.header_video_list_count))
                    .setText(String.format(countFormat, param));

            listView.setAdapter(new VideoAdapter(this));
            resetListContents(topic.getId());
        }
    }

    private void resetListContents(String topicId) {
        Log.d(LOG_TAG, "resetListContents");

        if (topicId != null) {
            // Set this.topicCursor to a cursor over the videos we need.
            User user = dataService.getAPIAdapter().getCurrentUser();
            String userId = user == null ? "" : user.getNickname();

            String sql = "select video._id, video.youtube_id, video.readable_id, video.title "
                    + ", uservideo.seconds_watched, uservideo.completed " + "from topicvideo, video "
                    + "left outer join uservideo on uservideo.video_id = video.readable_id and uservideo.user_id=? "
                    + "where topicvideo.topic_id=? and topicvideo.video_id=video.readable_id ";

            String[] selectionArgs;
            if (isShowingDownloadedVideosOnly()) {
                sql += " and video.download_status=? ";
                selectionArgs = new String[] { userId, topicId, String.valueOf(Video.DL_STATUS_COMPLETE) };
            } else {
                selectionArgs = new String[] { userId, topicId };
            }
            sql += "order by video.seq";

            if (topicCursor != null) {
                topicCursor.close();
            }
            topicCursor = this.dataService.getHelper().getReadableDatabase().rawQuery(sql, selectionArgs);

            CursorAdapter adapter = getUnwrappedAdapter();
            if (adapter != null) {
                adapter.changeCursor(topicCursor);
            }
        }
    }

    public boolean isShowingDownloadedVideosOnly() {
        return isShowingDownloadedVideosOnly;
    }

    protected Where<Video, String> addToQuery(Where<Video, String> where) throws SQLException {
        if (isShowingDownloadedVideosOnly()) {
            // This causes non-downloaded videos not to appear in the list. To just disable them, use the VideoAdapter instead.
            where.and().eq("download_status", Video.DL_STATUS_COMPLETE);
        }
        return where;
    }

    // map of id:progressbar; put in onBindView

    // update on download progress updates

    private static class Renderer extends ThumbnailViewRenderer {

        private final CursorAdapter mAdapter;
        private int titleColumn, watchedColumn, completedColumn;
        private boolean prepared = false;
        private Map<String, Integer> currentDownloadStatus = new HashMap<String, Integer>();

        public Renderer(android.support.v4.widget.CursorAdapter adapter, ThumbnailManager thumbnailManager,
                int cacheCapacity) {
            super(2, R.id.thumbnail, thumbnailManager, Thumbnail.QUALITY_HIGH, cacheCapacity);
            mAdapter = adapter;
        }

        @Override
        protected void prepare(View view, Param param, int immediatePassHint) {
            super.prepare(view, param, immediatePassHint);

            Cursor cursor = (Cursor) mAdapter.getItem(param.cursorPosition);

            if (!prepared) {
                titleColumn = cursor.getColumnIndex("title");
                watchedColumn = cursor.getColumnIndex("seconds_watched");
                completedColumn = cursor.getColumnIndex("completed");
                prepared = true;
            }

            String title = cursor.getString(titleColumn);

            TextView titleView = (TextView) view.findViewById(R.id.list_video_title);
            ImageView iconView = (ImageView) view.findViewById(R.id.complete_icon);
            ProgressBar bar = (ProgressBar) view.findViewById(R.id.list_video_dl_progress);
            String youtubeId = param.youtubeId;
            bar.setTag(youtubeId);
            updateBar(bar);

            // User view completion icon.
            int watched = 0;
            boolean complete = false;
            try {
                watched = cursor.getInt(watchedColumn);
                complete = cursor.getInt(completedColumn) != 0;
            } catch (Exception e) {
                // Swallow. This will be due to null values not making their way through getInt in some implementations.
            }
            int resId = complete ? R.drawable.video_indicator_complete
                    : watched > 0 ? R.drawable.video_indicator_started : R.drawable.empty_icon;

            iconView.setImageResource(resId);
            titleView.setText(title);
        }

        public void onCursorChanged() {
            prepared = false;
        }

        public void updateBar(ProgressBar bar) {
            String youtubeId = (String) bar.getTag();
            Integer progress = currentDownloadStatus.get(youtubeId);
            if (progress == null) {
                bar.setVisibility(View.GONE);
            } else {
                switch (progress) {
                case 100:
                    bar.setVisibility(View.GONE);
                    break;
                case 0:
                    bar.setIndeterminate(true);
                    bar.setVisibility(View.VISIBLE);
                    break;
                default:
                    bar.setIndeterminate(false);
                    bar.setProgress(progress);
                    bar.setVisibility(View.VISIBLE);
                }
            }
        }

        public void setStatus(Map<String, Integer> status) {
            currentDownloadStatus = status;
        }

    }

    class VideoAdapter extends CursorAdapter {

        private final LayoutInflater inflater;
        private final Renderer renderer;

        private final ArrayList<ProgressBar> bars = new ArrayList<ProgressBar>();

        public VideoAdapter(Context context) {
            super(context, null, 0);
            inflater = LayoutInflater.from(context);

            Runtime rt = Runtime.getRuntime();
            long maxMemory = rt.maxMemory();
            Log.v(LOG_TAG, "maxMemory:" + Long.toString(maxMemory));

            // Want to use at most about 1/2 of available memory for thumbs.
            // In SAT Math category (116 videos), with a heap size of 48MB, this setting
            // allows 109 thumbs to be cached resulting in total heap usage around 34MB.
            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 24 thumbs on Fire HD 7, 36 on transformer.
            usableMemory /= getResources().getDisplayMetrics().density;

            int thumbSize = 480 * 360 * 4; // QUALITY_HIGH at 4 bytes per pixel
            int maxCachedCount = (int) (usableMemory / thumbSize);

            renderer = new Renderer(this, thumbnailManager, maxCachedCount);
        }

        public void setStatus(Map<String, Integer> status) {
            renderer.setStatus(status);
        }

        private void updateBars() {
            for (ProgressBar bar : bars) {
                renderer.updateBar(bar);
            }
        }

        @Override
        public void bindView(View view, Context context, Cursor cursor) {
            renderer.renderView(view,
                    new Param(cursor.getPosition(), cursor.getString(cursor.getColumnIndex("youtube_id"))));
        }

        @Override
        public View newView(Context context, Cursor cursor, ViewGroup root) {
            View view = inflater.inflate(R.layout.list_video, root, false);
            ProgressBar bar = (ProgressBar) view.findViewById(R.id.list_video_dl_progress);
            bars.add(bar);
            return view;
        }

        @Override
        public boolean areAllItemsEnabled() {
            // When showing downloaded videos only, we eliminate them from the query instead of disabling them.
            return true;
        }

        @Override
        public boolean isEnabled(int position) {
            return true;
        }

        @Override
        public void changeCursor(Cursor newCursor) {
            super.changeCursor(newCursor);
            renderer.onCursorChanged();
            updateBars();
        }

    }

}