Java tutorial
/* 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; } } }