Merge pull request #777 from mfietz/feature/download-log-retry

Retry failed downloads in the download log
This commit is contained in:
Tom Hennen 2015-05-02 08:25:18 -04:00
commit 252f80de78
5 changed files with 239 additions and 89 deletions

View File

@ -2,20 +2,34 @@ package de.danoeh.antennapod.adapter;
import android.content.Context; import android.content.Context;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.BaseAdapter; import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import com.joanzapata.android.iconify.Iconify;
import java.util.Date;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
/** Displays a list of DownloadStatus entries. */ /** Displays a list of DownloadStatus entries. */
public class DownloadLogAdapter extends BaseAdapter { public class DownloadLogAdapter extends BaseAdapter {
private final String TAG = "DownloadLogAdapter";
private Context context; private Context context;
private ItemAccess itemAccess; private ItemAccess itemAccess;
@ -35,11 +49,11 @@ public class DownloadLogAdapter extends BaseAdapter {
LayoutInflater inflater = (LayoutInflater) context LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE); .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.downloadlog_item, parent, false); convertView = inflater.inflate(R.layout.downloadlog_item, parent, false);
holder.icon = (TextView) convertView.findViewById(R.id.txtvIcon);
holder.retry = (Button) convertView.findViewById(R.id.btnRetry);
holder.date = (TextView) convertView.findViewById(R.id.txtvDate);
holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
holder.type = (TextView) convertView.findViewById(R.id.txtvType); holder.type = (TextView) convertView.findViewById(R.id.txtvType);
holder.date = (TextView) convertView.findViewById(R.id.txtvDate);
holder.successful = (TextView) convertView
.findViewById(R.id.txtvStatus);
holder.reason = (TextView) convertView holder.reason = (TextView) convertView
.findViewById(R.id.txtvReason); .findViewById(R.id.txtvReason);
convertView.setTag(holder); convertView.setTag(holder);
@ -62,33 +76,99 @@ public class DownloadLogAdapter extends BaseAdapter {
status.getCompletionDate().getTime(), status.getCompletionDate().getTime(),
System.currentTimeMillis(), 0, 0)); System.currentTimeMillis(), 0, 0));
if (status.isSuccessful()) { if (status.isSuccessful()) {
holder.successful.setTextColor(convertView.getResources().getColor( holder.icon.setTextColor(convertView.getResources().getColor(
R.color.download_success_green)); R.color.download_success_green));
holder.successful.setText(R.string.download_successful); holder.icon.setText("{fa-check-circle}");
Iconify.addIcons(holder.icon);
holder.retry.setVisibility(View.GONE);
holder.reason.setVisibility(View.GONE); holder.reason.setVisibility(View.GONE);
} else { } else {
holder.successful.setTextColor(convertView.getResources().getColor( holder.icon.setTextColor(convertView.getResources().getColor(
R.color.download_failed_red)); R.color.download_failed_red));
holder.successful.setText(R.string.download_failed); holder.icon.setText("{fa-times-circle}");
Iconify.addIcons(holder.icon);
String reasonText = status.getReason().getErrorString(context); String reasonText = status.getReason().getErrorString(context);
if (status.getReasonDetailed() != null) { if (status.getReasonDetailed() != null) {
reasonText += ": " + status.getReasonDetailed(); reasonText += ": " + status.getReasonDetailed();
} }
holder.reason.setText(reasonText); holder.reason.setText(reasonText);
holder.reason.setVisibility(View.VISIBLE); holder.reason.setVisibility(View.VISIBLE);
if(status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE &&
!newerWasSuccessful(position, status.getFeedfileType(), status.getFeedfileId())) {
holder.retry.setVisibility(View.VISIBLE);
holder.retry.setText("{fa-repeat}");
Iconify.addIcons(holder.retry);
holder.retry.setOnClickListener(clickListener);
ButtonHolder btnHolder;
if(holder.retry.getTag() != null) {
btnHolder = (ButtonHolder) holder.retry.getTag();
} else {
btnHolder = new ButtonHolder();
}
btnHolder.typeId = status.getFeedfileType();
btnHolder.id = status.getFeedfileId();
holder.retry.setTag(btnHolder);
} else {
holder.retry.setVisibility(View.GONE);
holder.retry.setOnClickListener(null);
holder.retry.setTag(null);
}
} }
return convertView; return convertView;
} }
private final View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
ButtonHolder holder = (ButtonHolder) v.getTag();
if(holder.typeId == Feed.FEEDFILETYPE_FEED) {
Feed feed = DBReader.getFeed(context, holder.id);
feed.setLastUpdate(new Date(0)); // force refresh
try {
DBTasks.refreshFeed(context, feed);
} catch (DownloadRequestException e) {
e.printStackTrace();
}
} else if(holder.typeId == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
FeedMedia media = DBReader.getFeedMedia(context, holder.id);
try {
DBTasks.downloadFeedItems(context, media.getItem());
Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show();
} catch (DownloadRequestException e) {
e.printStackTrace();
DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage());
}
} else {
Log.wtf(TAG, "Unexpected type id: " + holder.typeId);
}
v.setVisibility(View.GONE);
}
};
private boolean newerWasSuccessful(int position, int feedTypeId, long id) {
for (int i = 0; i < position; i++) {
DownloadStatus status = getItem(i);
if (status.getFeedfileType() == feedTypeId && status.getFeedfileId() == id &&
status.isSuccessful()) return true;
}
return false;
}
static class Holder { static class Holder {
TextView icon;
Button retry;
TextView title; TextView title;
TextView type; TextView type;
TextView date; TextView date;
TextView successful;
TextView reason; TextView reason;
} }
static class ButtonHolder {
int typeId;
long id;
}
@Override @Override
public int getCount() { public int getCount() {
return itemAccess.getCount(); return itemAccess.getCount();
@ -104,9 +184,9 @@ public class DownloadLogAdapter extends BaseAdapter {
return position; return position;
} }
public static interface ItemAccess { public interface ItemAccess {
public int getCount(); int getCount();
public DownloadStatus getItem(int position); DownloadStatus getItem(int position);
} }
} }

View File

@ -1,78 +1,84 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:paddingTop="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
tools:background="@android:color/darker_gray"> tools:background="@android:color/darker_gray">
<RelativeLayout <TextView
android:layout_width="match_parent" android:id="@+id/txtvIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:textSize="48sp"
tools:text="[Icon]"
android:gravity="center" />
<Button
android:id="@+id/btnRetry"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/listitem_threeline_horizontalpadding" android:layout_below="@id/txtvIcon"
android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" android:layout_alignLeft="@id/txtvIcon"
android:layout_marginTop="@dimen/listitem_threeline_verticalpadding"> android:layout_alignRight="@id/txtvIcon"
android:layout_marginTop="8dp"
tools:text="↻" />
<TextView <TextView
android:id="@+id/txtvType" android:id="@+id/txtvType"
style="@style/AntennaPod.TextView.ListItemSecondaryTitle" style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginLeft="@dimen/listitem_threeline_textleftpadding"
tools:text="Media file"
tools:background="@android:color/holo_green_dark" />
<TextView
android:id="@+id/txtvTitle"
style="@style/AntennaPod.TextView.ListItemPrimaryTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@id/txtvType"
tools:text="Download item title"
tools:background="@android:color/holo_blue_light" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" android:layout_alignParentTop="true"
android:layout_marginLeft="@dimen/listitem_threeline_horizontalpadding" android:layout_alignParentRight="true"
android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding"> android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
tools:text="Media file"
tools:background="@android:color/holo_green_dark" />
<TextView <TextView
android:id="@+id/txtvDate" android:id="@+id/txtvTitle"
style="@style/AntennaPod.TextView.ListItemSecondaryTitle" style="@style/AntennaPod.TextView.ListItemPrimaryTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_alignParentTop="true"
android:layout_marginRight="8dp" android:layout_toRightOf="@id/txtvIcon"
tools:text="January 23" android:layout_toLeftOf="@id/txtvType"
tools:background="@android:color/holo_green_dark" /> android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
android:minLines="1"
android:maxLines="2"
tools:text="Download item title"
tools:background="@android:color/holo_blue_light" />
<TextView <TextView
android:id="@+id/txtvStatus" android:id="@+id/txtvDate"
style="@style/AntennaPod.TextView.ListItemSecondaryTitle" style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentRight="true" android:layout_toRightOf="@id/txtvIcon"
tools:text="successful" android:layout_below="@id/txtvTitle"
tools:background="@android:color/holo_green_dark" /> android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
</RelativeLayout> tools:text="January 23"
tools:background="@android:color/holo_green_dark" />
<TextView <TextView
android:id="@+id/txtvReason" android:id="@+id/txtvReason"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" android:layout_below="@id/txtvDate"
android:layout_marginLeft="@dimen/listitem_threeline_horizontalpadding" android:layout_toRightOf="@id/txtvIcon"
android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" android:layout_marginLeft="8dp"
android:textColor="?android:attr/textColorTertiary" android:textColor="?android:attr/textColorTertiary"
android:textSize="@dimen/text_size_micro" android:textSize="@dimen/text_size_micro"
tools:text="@string/design_time_downloaded_log_failure_reason" tools:text="@string/design_time_downloaded_log_failure_reason"
tools:background="@android:color/holo_green_dark" /> tools:background="@android:color/holo_green_dark" />
</LinearLayout> </RelativeLayout>

View File

@ -939,6 +939,13 @@ public class DownloadService extends Service {
if (successful) { if (successful) {
// we create a 'successful' download log if the feed's last refresh failed
List<DownloadStatus> log = DBReader.getFeedDownloadLog(DownloadService.this, feed);
if(log.size() > 0 && log.get(0).isSuccessful() == false) {
saveDownloadStatus(new DownloadStatus(feed,
feed.getHumanReadableIdentifier(), DownloadError.SUCCESS, successful,
reasonDetailed));
}
return Pair.create(request, result); return Pair.create(request, result);
} else { } else {
numberOfDownloads.decrementAndGet(); numberOfDownloads.decrementAndGet();

View File

@ -327,6 +327,21 @@ public final class DBReader {
return feed; return feed;
} }
private static DownloadStatus extractDownloadStatusFromCursorRow(final Cursor cursor) {
long id = cursor.getLong(PodDBAdapter.KEY_ID_INDEX);
long feedfileId = cursor.getLong(PodDBAdapter.KEY_FEEDFILE_INDEX);
int feedfileType = cursor.getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX);
boolean successful = cursor.getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0;
int reason = cursor.getInt(PodDBAdapter.KEY_REASON_INDEX);
String reasonDetailed = cursor.getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX);
String title = cursor.getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX);
Date completionDate = new Date(cursor.getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX));
return new DownloadStatus(id, title, feedfileId,
feedfileType, successful, DownloadError.fromCode(reason), completionDate,
reasonDetailed);
}
private static FeedItem getMatchingItemForMedia(long itemId, private static FeedItem getMatchingItemForMedia(long itemId,
List<FeedItem> items) { List<FeedItem> items) {
@ -565,27 +580,7 @@ public final class DBReader {
if (logCursor.moveToFirst()) { if (logCursor.moveToFirst()) {
do { do {
long id = logCursor.getLong(PodDBAdapter.KEY_ID_INDEX); downloadLog.add(extractDownloadStatusFromCursorRow(logCursor));
long feedfileId = logCursor
.getLong(PodDBAdapter.KEY_FEEDFILE_INDEX);
int feedfileType = logCursor
.getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX);
boolean successful = logCursor
.getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0;
int reason = logCursor.getInt(PodDBAdapter.KEY_REASON_INDEX);
String reasonDetailed = logCursor
.getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX);
String title = logCursor
.getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX);
Date completionDate = new Date(
logCursor
.getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX)
);
downloadLog.add(new DownloadStatus(id, title, feedfileId,
feedfileType, successful, DownloadError.fromCode(reason), completionDate,
reasonDetailed));
} while (logCursor.moveToNext()); } while (logCursor.moveToNext());
} }
logCursor.close(); logCursor.close();
@ -593,6 +588,60 @@ public final class DBReader {
return downloadLog; return downloadLog;
} }
/**
* Loads the download log for a particular feed from the database.
*
* @param context A context that is used for opening a database connection.
* @param feed Feed for which the download log is loaded
* @return A list with DownloadStatus objects that represent the feed's download log,
* newest events first.
*/
public static List<DownloadStatus> getFeedDownloadLog(Context context, Feed feed) {
Log.d(TAG, "getFeedDownloadLog(CONTEXT, " + feed.toString() + ")");
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feed.getId());
List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>(
cursor.getCount());
if (cursor.moveToFirst()) {
do {
downloadLog.add(extractDownloadStatusFromCursorRow(cursor));
} while (cursor.moveToNext());
}
cursor.close();
Collections.sort(downloadLog, new DownloadStatusComparator());
return downloadLog;
}
/**
* Loads the download log for a particular feed media from the database.
*
* @param context A context that is used for opening a database connection.
* @param media Feed media for which the download log is loaded
* @return A list with DownloadStatus objects that represent the feed media's download log,
* newest events first.
*/
public static List<DownloadStatus> getFeedMediaDownloadLog(Context context, FeedMedia media) {
Log.d(TAG, "getFeedDownloadLog(CONTEXT, " + media.toString() + ")");
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
Cursor cursor = adapter.getDownloadLog(FeedMedia.FEEDFILETYPE_FEEDMEDIA, media.getId());
List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>(
cursor.getCount());
if (cursor.moveToFirst()) {
do {
downloadLog.add(extractDownloadStatusFromCursorRow(cursor));
} while (cursor.moveToNext());
}
cursor.close();
Collections.sort(downloadLog, new DownloadStatusComparator());
return downloadLog;
}
/** /**
* Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over * Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over
* {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)} if only metadata about * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)} if only metadata about

View File

@ -977,6 +977,14 @@ public class PodDBAdapter {
return c; return c;
} }
public final Cursor getDownloadLog(final int feedFileType, final long feedFileId) {
final String query = "SELECT * FROM " + TABLE_NAME_DOWNLOAD_LOG +
" WHERE " + KEY_FEEDFILE + "=" + feedFileId + " AND " + KEY_FEEDFILETYPE + "=" + feedFileType
+ " ORDER BY " + KEY_ID + " DESC";
Cursor c = db.rawQuery(query, null);
return c;
}
public final Cursor getDownloadLogCursor(final int limit) { public final Cursor getDownloadLogCursor(final int limit) {
Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null, Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null,
null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit); null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit);