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

Java tutorial

Introduction

Here is the source code for com.concentricsky.android.khanacademy.app.ManageDownloadsActivity.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_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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import android.app.ActionBar;
import android.app.AlertDialog;
import android.app.DownloadManager;
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.database.MergeCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.widget.CursorAdapter;
import android.support.v4.widget.SimpleCursorAdapter;
import android.util.SparseBooleanArray;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
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.GridView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.concentricsky.android.khan.R;
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.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.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;

public class ManageDownloadsActivity extends KADataServiceProviderActivityBase {

    public static final String LOG_TAG = ManageDownloadsActivity.class.getSimpleName();

    // TODO : saveInstanceState

    private GridView gridView;
    private KADataService dataService;
    private ActionMode actionMode;
    private LocalBroadcastManager broadcastManager;
    private CursorAdapter displayOptionsAdapter;
    private Menu menu;

    /** Some topics share titles (but not parent hierarchies). Since we display only titles here,
     *  merging their children is less confusing than listing the same title twice. */
    private String topicTitleFilter;

    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (ACTION_LIBRARY_UPDATE.equals(intent.getAction())) {
                Log.d(LOG_TAG, "library update broadcast received");
                // TODO
            } else if (ACTION_BADGE_EARNED.equals(intent.getAction()) && dataService != null) {
                Badge badge = (Badge) intent.getSerializableExtra(EXTRA_BADGE);
                dataService.getAPIAdapter().toastBadge(badge);
            } else if (Constants.ACTION_OFFLINE_VIDEO_SET_CHANGED.equals(intent.getAction())) {
                ((CursorAdapter) gridView.getAdapter()).changeCursor(getCursor());
                setCancelButtonEnabled(areDownloadsEnqueued());
                setupListNavigation();
            } else if (ACTION_DOWNLOAD_PROGRESS_UPDATE.equals(intent.getAction())) {
                @SuppressWarnings("unchecked")
                Map<String, Integer> status = (Map<String, Integer>) intent.getSerializableExtra(EXTRA_STATUS);
                Adapter adapter = (Adapter) gridView.getAdapter();
                adapter.setCurrentDownloadStatus(status);
                adapter.updateBars();
            } else if (ACTION_TOAST.equals(intent.getAction())) {
                Toast.makeText(ManageDownloadsActivity.this, intent.getStringExtra(EXTRA_MESSAGE),
                        Toast.LENGTH_SHORT).show();
            }
        }

    };

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

            if (itemId < 0) {
                filterByTopicTitle(null);
            } else {
                Cursor c = (Cursor) displayOptionsAdapter.getItem(itemPosition);
                String topicTitle = c.getString(c.getColumnIndex("title"));
                filterByTopicTitle(topicTitle);
            }

            return true;
        }
    };

    private AdapterView.OnItemClickListener itemClickListener = new AdapterView.OnItemClickListener() {

        @Override
        public void onItemClick(AdapterView<?> grid, View view, int position, long id) {
            // If the list is currently unfiltered, then we query for parents of the video and use the first topic that comes up.
            // Otherwise, we have a topic id.
            Cursor c = (Cursor) grid.getItemAtPosition(position);

            String videoId = c.getString(c.getColumnIndex("readable_id"));
            Video video = new Video();
            video.setReadable_id(videoId);

            String topicId = null;
            if (topicTitleFilter != null) {
                try {
                    List<Topic> topics = dataService.getHelper().getTopicDao().queryForEq("title",
                            topicTitleFilter);
                    if (topics != null && topics.size() > 0) {
                        topicId = topics.get(0).getId();
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

            if (topicId == null) {
                String sql = "select topic_id from topicvideo where video_id=? limit 1";
                String[] selectionArgs = { videoId };
                c = dataService.getHelper().getReadableDatabase().rawQuery(sql, selectionArgs);
                if (c.moveToFirst()) {
                    topicId = c.getString(0);
                }
                c.close();
            }

            if (topicId != null) {
                launchVideoDetailActivity(video, topicId);
            }
        }
    };

    private AbsListView.MultiChoiceModeListener multiChoiceModeListener = new AbsListView.MultiChoiceModeListener() {

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            updateTitle(mode);
            return true;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            actionMode = null;
        }

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            Log.d(LOG_TAG, "onCreateActionMode");
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.downloads_actionmode, menu);
            actionMode = mode;
            return true;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            Log.d(LOG_TAG, "onActionItemClicked");

            switch (item.getItemId()) {
            case R.id.menu_delete:
                confirmAndDelete();
                return true;
            case android.R.id.selectAll:
                Log.d(LOG_TAG, "select all");
                selectAll();
                return true;
            default:
                return false;
            }
        }

        @Override
        public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
            Log.d(LOG_TAG, "onItemCheckedStateChanged");
            updateTitle(mode);
        }

        private void updateTitle(ActionMode mode) {
            int childCount = gridView.getCount();
            int checkedCount = gridView.getCheckedItemCount();
            mode.setTitle(String.format("%d of %d selected.", checkedCount, childCount));
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_manage_downloads);
        broadcastManager = LocalBroadcastManager.getInstance(this);
    }

    @Override
    protected void onStart() {
        super.onStart();

        gridView = (GridView) findViewById(R.id.grid);
        gridView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL);
        gridView.setMultiChoiceModeListener(multiChoiceModeListener);
        gridView.setOnItemClickListener(itemClickListener);

        View emptyView = getLayoutInflater().inflate(R.layout.listview_empty, null, false);
        ((TextView) emptyView.findViewById(R.id.text_list_empty)).setText(R.string.msg_no_downloaded_videos);
        ViewGroup.LayoutParams p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        addContentView(emptyView, p);

        gridView.setEmptyView(emptyView);

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

                CursorAdapter adapter = new Adapter(ManageDownloadsActivity.this, null, 0,
                        dataService.getThumbnailManager());
                gridView.setAdapter(adapter);

                new AsyncTask<Void, Void, Cursor>() {
                    @Override
                    protected Cursor doInBackground(Void... arg) {
                        return getCursor();
                    }

                    @Override
                    protected void onPostExecute(Cursor cursor) {
                        ((CursorAdapter) gridView.getAdapter()).changeCursor(cursor);
                    }
                }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

                final ActionBar ab = getActionBar();
                ab.setDisplayHomeAsUpEnabled(true);

                ab.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
                ab.setTitle("");

                setupListNavigation();

                // The receiver performs actions that require a dataService, so register it here.
                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);
                broadcastManager.registerReceiver(receiver, filter);
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        this.menu = menu;
        getMenuInflater().inflate(R.menu.downloads_menu, menu);
        setCancelButtonEnabled(areDownloadsEnqueued());
        return true;
    }

    private void setCancelButtonEnabled(boolean enabled) {
        if (menu != null) {
            menu.findItem(R.id.menu_cancel).setEnabled(enabled).setVisible(enabled);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case android.R.id.home:
            finish();
            return true;
        case R.id.menu_cancel:
            confirmAndCancelDownloads();
            return true;
        }
        return false;
    }

    @Override
    protected void onStop() {
        getActionBar().setListNavigationCallbacks(null, null);

        gridView.setMultiChoiceModeListener(null);
        gridView.setOnItemClickListener(null);
        Adapter adapter = (Adapter) gridView.getAdapter();
        if (adapter != null) {
            Cursor cursor = adapter.getCursor();
            if (cursor != null && !cursor.isClosed()) {
                cursor.close();
            }
            adapter.renderer.stop();
            adapter.renderer.clearCache();
        }
        gridView.setAdapter(null);

        if (displayOptionsAdapter != null) {
            Cursor cursor = displayOptionsAdapter.getCursor();
            if (cursor != null && !cursor.isClosed()) {
                cursor.close();
            }
        }

        broadcastManager.unregisterReceiver(receiver);
        super.onStop();
    }

    private Cursor getCursor(SQLiteOpenHelper helper, User currentUser, String topicTitle) {
        String userId = currentUser == null ? "" : currentUser.getNickname();

        String sql = "select distinct(video._id) as _id, video.youtube_id, video.readable_id, video.title, video.dlm_id "
                + ", uservideo.seconds_watched, uservideo.completed " +
                // If we join on topicvideo without filtering to a single topic id, we get a giant n^2 query that takes forever.
                "from video" + (topicTitle != null ? ", topicvideo, topic " : " ")
                + "left outer join uservideo on uservideo.video_id = video.readable_id and uservideo.user_id=? "
                + "where video.download_status>? ";

        String[] selectionArgs;
        if (topicTitle != null) {
            sql += " and topicvideo.topic_id=topic._id and topic.title=? and topicvideo.video_id=video.readable_id ";
            selectionArgs = new String[] { userId, String.valueOf(Video.DL_STATUS_NOT_STARTED), topicTitle };
        } else {
            selectionArgs = new String[] { userId, String.valueOf(Video.DL_STATUS_NOT_STARTED) };
        }
        sql += "order by video.parentTopic_id, video.seq";

        return helper.getReadableDatabase().rawQuery(sql, selectionArgs);
    }

    /**
     * Build a cursor appropriate for our current state.
     * 
     * Uses {@link getCursor} or {@link getCursor} based on the current actionbar nav dropdown selection.
     * 
     * @return
     */
    private Cursor getCursor() {
        return getCursor(dataService.getHelper(), dataService.getAPIAdapter().getCurrentUser(), topicTitleFilter);
    }

    private void filterByTopicTitle(String topicTitle) {
        topicTitleFilter = topicTitle;

        new AsyncTask<Void, Void, Cursor>() {
            @Override
            protected Cursor doInBackground(Void... arg) {
                Cursor cursor = getCursor();
                return cursor;
            }

            @Override
            protected void onPostExecute(Cursor cursor) {
                ((CursorAdapter) gridView.getAdapter()).changeCursor(cursor);
            }
        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void setupListNavigation() {
        if (displayOptionsAdapter == null) {
            displayOptionsAdapter = getDisplayOptionsAdapter(null);
            displayOptionsAdapter.changeCursor(null);
            getActionBar().setListNavigationCallbacks(displayOptionsAdapter, navListener);
        }

        new AsyncTask<Void, Void, Cursor>() {
            @Override
            protected Cursor doInBackground(Void... arg0) {
                return getDisplayOptionsCursor(dataService.getHelper());
            }

            @Override
            protected void onPostExecute(Cursor cursor) {
                displayOptionsAdapter.changeCursor(cursor);
            }
        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private Cursor getDisplayOptionsCursor(SQLiteOpenHelper helper) {
        SQLiteDatabase db = helper.getReadableDatabase();

        String sql = "select distinct topic._id as _id, topic.title as title from topic, topicvideo, video where video.download_status>? and topicvideo.video_id=video.readable_id and topicvideo.topic_id=topic._id group by title";
        String[] selectionArgs = { String.valueOf(Video.DL_STATUS_NOT_STARTED) };
        Cursor mainCursor = db.rawQuery(sql, selectionArgs);

        sql = "select '-1' as _id, 'All Videos' as title";
        Cursor headerCursor = db.rawQuery(sql, null);

        MergeCursor cursor = new MergeCursor(new Cursor[] { headerCursor, mainCursor });
        return cursor;
    }

    private CursorAdapter getDisplayOptionsAdapter(Cursor c) {
        String[] from = { "title" };
        int[] to = { android.R.id.text1 };
        return new SimpleCursorAdapter(getActionBar().getThemedContext(), android.R.layout.simple_list_item_1, c,
                from, to, 0);
    }

    private void selectAll() {
        int n = gridView.getCount();
        for (int i = 0; i < n; ++i) {
            gridView.setItemChecked(i, true);
        }
    }

    private void confirmAndDelete() {
        View contentView = getLayoutInflater().inflate(R.layout.dialog_confirm_delete, null, false);
        ListView list = (ListView) contentView.findViewById(R.id.dialog_confirm_delete_list);

        ArrayList<String> titles = new ArrayList<String>();
        final HashSet<Video> videos = new HashSet<Video>();

        SparseBooleanArray positions = gridView.getCheckedItemPositions();
        int n = positions.size();
        for (int i = 0; i < n; ++i) {
            Cursor c = (Cursor) gridView.getItemAtPosition(positions.keyAt(i));
            Video v = new Video();
            v.setReadable_id(c.getString(c.getColumnIndex("readable_id")));
            v.setYoutube_id(c.getString(c.getColumnIndex("youtube_id")));
            v.setDlm_id(c.getLong(c.getColumnIndex("dlm_id")));
            videos.add(v);
            titles.add(c.getString(c.getColumnIndex("title")));
        }

        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.dialog_confirm_delete_item);
        adapter.addAll(titles);
        list.setAdapter(adapter);

        new AlertDialog.Builder(this).setView(contentView).setMessage(getString(R.string.msg_delete_videos))
                .setPositiveButton(getString(R.string.button_confirm_delete),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int id) {
                                deleteItems(videos);
                                if (actionMode != null) {
                                    actionMode.finish();
                                }
                            }
                        })
                .setNegativeButton(getString(R.string.button_cancel), null).show();
    }

    private void confirmAndCancelDownloads() {
        new AlertDialog.Builder(this).setMessage(getString(R.string.msg_cancel_downloads))
                .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int id) {
                        dataService.getOfflineVideoManager().cancelAllVideoDownloads();
                    }
                }).show();
    }

    private void deleteItems(Set<Video> videos) {
        dataService.getOfflineVideoManager().deleteOfflineVideos(videos);
    }

    private void launchVideoDetailActivity(Video video, String parentTopicId) {
        // This may be a video in a different topic from where the user came from.
        // We synthesize the correct back stack for this video.

        Stack<Topic> stack = new Stack<Topic>();
        Topic topic = null;
        try {
            Dao<Topic, String> topicDao = getDataService().getHelper().getTopicDao();
            topic = topicDao.queryForId(parentTopicId);
            while (topic != null) {
                stack.push(topic);
                topicDao.refresh(topic);
                topic = topic.getParentTopic();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ServiceUnavailableException e) {
            e.printStackTrace();
        }

        TaskStackBuilder t = TaskStackBuilder.create(this);
        Intent intent;
        while (!stack.isEmpty()) {
            topic = stack.pop();
            if (topic.getParentTopic() == null) {
                // Root topic gets the HomeActivity.
                intent = new Intent(this, HomeActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            } else if (stack.isEmpty()) {
                // The video's immediate parent topic gets a VideoList.
                intent = new Intent(this, VideoListActivity.class);
                intent.putExtra(PARAM_TOPIC_ID, topic.getId());
            } else {
                // All other intermediate topics get TopicLists.
                intent = new Intent(this, TopicListActivity.class);
                intent.putExtra(PARAM_TOPIC_ID, topic.getId());
            }
            t.addNextIntent(intent);
        }

        // Add a manage downloads activity also, for back. It is skipped when going "up" from videos.
        intent = new Intent(this, ManageDownloadsActivity.class);
        t.addNextIntent(intent);

        intent = new Intent(this, VideoDetailActivity.class);
        intent.putExtra(PARAM_VIDEO_ID, video.getId());
        intent.putExtra(PARAM_TOPIC_ID, parentTopicId);
        t.addNextIntent(intent);
        t.startActivities();

        /*
            TaskStackBuilder just does this:
                
           intents[0].addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
           IntentCompat.FLAG_ACTIVITY_CLEAR_TASK |
           IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME);
               
        and then uses Activity#startActivities. Support library fallback pre-honeycomb is to just start the top
        activity and allow back to progress back through the actual back stack (no synthesis).
         */

        //      startActivity(intent);
    }

    private boolean areDownloadsEnqueued() {
        DownloadManager.Query q = new DownloadManager.Query();
        q.setFilterByStatus(
                DownloadManager.STATUS_PAUSED | DownloadManager.STATUS_PENDING | DownloadManager.STATUS_RUNNING);
        Cursor c = getDownloadManager().query(q);
        boolean result = c.getCount() > 0;
        c.close();
        return result;
    }

    private DownloadManager getDownloadManager() {
        return (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
    }

    private static class Adapter extends CursorAdapter {

        private final LayoutInflater inflater;
        private final Renderer renderer;
        private int youtubeIdIndex = -1;
        private final List<ProgressBar> bars = new ArrayList<ProgressBar>();

        public Adapter(Context context, Cursor c, int flags, ThumbnailManager thumbnailManager) {
            super(context, c, flags);
            inflater = LayoutInflater.from(context);

            // 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 maxMemory = Runtime.getRuntime().maxMemory();
            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 72 thumbs on Fire HD 7, 109 on transformer. About 20-27 fit on screen.
            usableMemory /= context.getResources().getDisplayMetrics().density;

            int thumbSize = 320 * 180 * 4; // QUALITY_MEDIUM at 4 bytes per pixel
            int maxCachedCount = (int) (usableMemory / thumbSize);
            renderer = new Renderer(this, thumbnailManager, maxCachedCount);
        }

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

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

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

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

        @Override
        public void changeCursor(Cursor cursor) {
            youtubeIdIndex = -1;
            super.changeCursor(cursor);
            updateBars();
        }
    }

    private static class Renderer extends ThumbnailViewRenderer {

        private 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_MEDIUM, 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);
            bar.setTag(param.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 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 setCurrentDownloadStatus(Map<String, Integer> status) {
            currentDownloadStatus = status;
        }

    }

}