Squashed commit of the following:

commit 7d0e6d5c2dc75f8f1e54f4a1279c6b0cffa7b8cc
Author: daniel oeh <daniel.oeh@gmail.com>
Date:   Mon Nov 10 21:00:58 2014 +0100

    Made tests compile

commit 05c57df87dd469d8f64835700eefe2e3c87e04e4
Author: daniel oeh <daniel.oeh@gmail.com>
Date:   Mon Nov 10 20:56:58 2014 +0100

    Removed unused code

commit 0a1fbc9e6d9648646140e30dec0ec8389fb8d37f
Author: daniel oeh <daniel.oeh@gmail.com>
Date:   Thu Nov 6 15:48:24 2014 +0100

    Added UI controls to download more feed pages

    closes #245

commit 6486fb40f1d03887e264df95946f91f0a9cdac9b
Author: daniel oeh <daniel.oeh@gmail.com>
Date:   Thu Nov 6 14:49:43 2014 +0100

    Added support for downloading feed pages

commit e1faa06908bfd50f2aa0c28ee5118772c4281557
Author: daniel oeh <daniel.oeh@gmail.com>
Date:   Wed Nov 5 21:27:03 2014 +0100

    Added "paged" and "loadAllPages" attributes
This commit is contained in:
daniel oeh 2014-11-10 21:01:41 +01:00
parent 6035e8fee9
commit ad04a80ae7
21 changed files with 457 additions and 97 deletions

View File

@ -65,7 +65,7 @@ public class HttpDownloaderTest extends InstrumentationTestCase {
private Downloader download(String url, String title, boolean expectedResult, boolean deleteExisting, String username, String password, boolean deleteOnFail) {
FeedFile feedFile = setupFeedFile(url, title, deleteExisting);
DownloadRequest request = new DownloadRequest(feedFile.getFile_url(), url, title, 0, feedFile.getTypeAsInt(), username, password, deleteOnFail);
DownloadRequest request = new DownloadRequest(feedFile.getFile_url(), url, title, 0, feedFile.getTypeAsInt(), username, password, deleteOnFail, null);
Downloader downloader = new HttpDownloader(request);
downloader.call();
DownloadStatus status = downloader.getResult();

View File

@ -43,7 +43,7 @@ public class DBReaderTest extends InstrumentationTestCase {
private void expiredFeedListTestHelper(long lastUpdate, long expirationTime, boolean shouldReturn) {
final Context context = getInstrumentation().getTargetContext();
Feed feed = new Feed(0, new Date(lastUpdate), "feed", "link", "descr", null,
null, null, null, "feed", null, null, "url", false, new FlattrStatus());
null, null, null, "feed", null, null, "url", false, new FlattrStatus(), false, null);
feed.setItems(new ArrayList<FeedItem>());
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();

View File

@ -296,7 +296,7 @@ public class DBTasksTest extends InstrumentationTestCase {
final Context context = getInstrumentation().getTargetContext();
UserPreferences.setUpdateInterval(context, expirationTime);
Feed feed = new Feed(0, new Date(lastUpdate), "feed", "link", "descr", null,
null, null, null, "feed", null, null, "url", false, new FlattrStatus());
null, null, null, "feed", null, null, "url", false, new FlattrStatus(), false, null);
feed.setItems(new ArrayList<FeedItem>());
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();

View File

@ -32,7 +32,7 @@ public class DBTestUtils {
adapter.open();
for (int i = 0; i < numFeeds; i++) {
Feed f = new Feed(0, new Date(), "feed " + i, "link" + i, "descr", null, null,
null, null, "id" + i, null, null, "url" + i, false, new FlattrStatus());
null, null, "id" + i, null, null, "url" + i, false, new FlattrStatus(), false, null);
f.setItems(new ArrayList<FeedItem>());
for (int j = 0; j < numItems; j++) {
FeedItem item = new FeedItem(0, "item " + j, "id" + j, "link" + j, new Date(),

View File

@ -183,14 +183,14 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity {
url = URLChecker.prepareURL(url);
feed = new Feed(url, new Date());
if (username != null && password != null) {
feed.setPreferences(new FeedPreferences(0, true, username, password));
feed.setPreferences(new FeedPreferences(0, false, username, password));
}
String fileUrl = new File(getExternalCacheDir(),
FileNameGenerator.generateFileName(feed.getDownload_url()))
.toString();
feed.setFile_url(fileUrl);
final DownloadRequest request = new DownloadRequest(feed.getFile_url(),
feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED, username, password, true);
feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED, username, password, true, null);
downloader = new HttpDownloader(
request);
new Thread() {

View File

@ -13,7 +13,7 @@ public class StorageCallbacksImpl implements StorageCallbacks {
@Override
public int getDatabaseVersion() {
return 12;
return 13;
}
@Override
@ -103,5 +103,11 @@ public class StorageCallbacksImpl implements StorageCallbacks {
+ " ADD COLUMN " + PodDBAdapter.KEY_IMAGE
+ " INTEGER");
}
if (oldVersion <= 12) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ " ADD COLUMN " + PodDBAdapter.KEY_IS_PAGED + " INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ " ADD COLUMN " + PodDBAdapter.KEY_NEXT_PAGE_LINK + " TEXT");
}
}
}

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
@ -44,9 +43,11 @@ import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil;
import de.danoeh.antennapod.dialog.FeedItemDialog;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
@ -82,6 +83,8 @@ public class ItemlistFragment extends ListFragment {
private FeedItemDialog feedItemDialog;
private FeedItemDialog.FeedItemDialogSavedInstance feedItemDialogSavedInstance;
private MoreContentListFooterUtil listFooter;
private boolean isUpdatingFeed;
/**
@ -293,12 +296,17 @@ public class ItemlistFragment extends ListFragment {
if (isUpdatingFeed != updateRefreshMenuItemChecker.isRefreshing()) {
getActivity().supportInvalidateOptionsMenu();
}
if (listFooter != null) {
listFooter.setLoadingState(DownloadRequester.getInstance().isDownloadingFeeds());
}
}
private void onFragmentLoaded() {
if (adapter == null) {
getListView().setAdapter(null);
setupHeaderView();
setupFooterView();
adapter = new FeedItemlistAdapter(getActivity(), itemAccess, new DefaultActionButtonCallback(getActivity()), false);
setListAdapter(adapter);
downloadObserver = new DownloadObserver(getActivity(), new Handler(), downloadObserverCallback);
@ -313,6 +321,11 @@ public class ItemlistFragment extends ListFragment {
feedItemDialog = FeedItemDialog.newInstance(getActivity(), feedItemDialogSavedInstance);
}
getActivity().supportInvalidateOptionsMenu();
if (feed != null && feed.getNextPageLink() == null && listFooter != null) {
getListView().removeFooterView(listFooter.getRoot());
}
}
private DownloadObserver.Callback downloadObserverCallback = new DownloadObserver.Callback() {
@ -372,6 +385,34 @@ public class ItemlistFragment extends ListFragment {
});
}
private void setupFooterView() {
if (getListView() == null || feed == null) {
Log.e(TAG, "Unable to setup listview: listView = null or feed = null");
return;
}
if (feed.isPaged() && feed.getNextPageLink() != null) {
ListView lv = getListView();
LayoutInflater inflater = (LayoutInflater)
getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View header = inflater.inflate(R.layout.more_content_list_footer, lv, false);
lv.addFooterView(header);
listFooter = new MoreContentListFooterUtil(header);
listFooter.setClickListener(new MoreContentListFooterUtil.Listener() {
@Override
public void onClick() {
if (feed != null) {
try {
DBTasks.loadNextPageOfFeed(getActivity(), feed, false);
} catch (DownloadRequestException e) {
e.printStackTrace();
DownloadRequestErrorDialogCreator.newRequestErrorDialog(getActivity(), e.getMessage());
}
}
}
});
}
}
private FeedItemlistAdapter.ItemAccess itemAccess = new FeedItemlistAdapter.ItemAccess() {
@Override

View File

@ -41,6 +41,8 @@ public class FeedMenuHandler {
else
menu.findItem(R.id.support_item).setVisible(false);
menu.findItem(R.id.refresh_complete_item).setVisible(selectedFeed.isPaged());
return true;
}
@ -55,6 +57,9 @@ public class FeedMenuHandler {
case R.id.refresh_item:
DBTasks.refreshFeed(context, selectedFeed);
break;
case R.id.refresh_complete_item:
DBTasks.refreshCompleteFeed(context, selectedFeed);
break;
case R.id.mark_all_read_item:
DBWriter.markFeedRead(context, selectedFeed.getId());
break;

View File

@ -6,29 +6,35 @@
android:id="@+id/refresh_item"
android:icon="?attr/navigation_refresh"
android:menuCategory="container"
custom:showAsAction="ifRoom"
android:title="@string/refresh_label">
android:title="@string/refresh_label"
custom:showAsAction="ifRoom">
</item>
<item
android:id="@+id/refresh_complete_item"
android:menuCategory="container"
android:title="@string/load_complete_feed"
custom:showAsAction="collapseActionView">
</item>
<item
android:id="@+id/mark_all_read_item"
android:menuCategory="container"
custom:showAsAction="collapseActionView"
android:title="@string/mark_all_read_label">
android:title="@string/mark_all_read_label"
custom:showAsAction="collapseActionView">
</item>
<item
android:id="@+id/support_item"
android:menuCategory="container"
custom:showAsAction="collapseActionView"
android:title="@string/support_label"
android:visible="false">
android:visible="false"
custom:showAsAction="collapseActionView">
</item>
<item
android:id="@+id/remove_item"
android:menuCategory="container"
android:icon="?attr/content_discard"
custom:showAsAction="collapseActionView"
android:menuCategory="container"
android:title="@string/remove_feed_label"
android:visible="true">
android:visible="true"
custom:showAsAction="collapseActionView">
</item>
</menu>

View File

@ -3,6 +3,12 @@ package de.danoeh.antennapod.core.feed;
import android.content.Context;
import android.net.Uri;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import de.danoeh.antennapod.core.asynctask.PicassoImageResource;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBWriter;
@ -10,10 +16,6 @@ import de.danoeh.antennapod.core.util.EpisodeFilter;
import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
import de.danoeh.antennapod.core.util.flattr.FlattrThing;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Data Object for a whole feed
*
@ -58,12 +60,33 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
*/
private FeedPreferences preferences;
/**
* The page number that this feed is on. Only feeds with page number "0" should be stored in the
* database, feed objects with a higher page number only exist temporarily and should be merged
* into feeds with page number "0".
* <p/>
* This attribute's value is not saved in the database
*/
private int pageNr;
/**
* True if this is a "paged feed", i.e. there exist other feed files that belong to the same
* logical feed.
*/
private boolean paged;
/**
* Link to the next page of this feed. If this feed object represents a logical feed (i.e. a feed
* that is saved in the database) this might be null while still being a paged feed.
*/
private String nextPageLink;
/**
* This constructor is used for restoring a feed from the database.
*/
public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink,
String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
String downloadUrl, boolean downloaded, FlattrStatus status) {
String downloadUrl, boolean downloaded, FlattrStatus status, boolean paged, String nextPageLink) {
super(fileUrl, downloadUrl, downloaded);
this.id = id;
this.title = title;
@ -81,6 +104,8 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
this.feedIdentifier = feedIdentifier;
this.image = image;
this.flattrStatus = status;
this.paged = paged;
this.nextPageLink = nextPageLink;
items = new ArrayList<FeedItem>();
}
@ -92,7 +117,7 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
String downloadUrl, boolean downloaded) {
this(id, lastUpdate, title, link, description, paymentLink, author, language, type, feedIdentifier, image,
fileUrl, downloadUrl, downloaded, new FlattrStatus());
fileUrl, downloadUrl, downloaded, new FlattrStatus(), false, null);
}
/**
@ -270,6 +295,12 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
if (other.flattrStatus != null) {
flattrStatus = other.flattrStatus;
}
// this feed's nextPage might already point to a higher page, so we only update the nextPage value
// if this feed is not paged and the other feed is.
if (!this.paged && other.paged) {
this.paged = other.paged;
this.nextPageLink = other.nextPageLink;
}
}
public boolean compareWithOther(Feed other) {
@ -310,6 +341,12 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
return true;
}
}
if (other.isPaged() && !this.isPaged()) {
return true;
}
if (!StringUtils.equals(other.getNextPageLink(), this.getNextPageLink())) {
return true;
}
return false;
}
@ -374,13 +411,13 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
this.feedIdentifier = feedIdentifier;
}
public void setFlattrStatus(FlattrStatus status) {
this.flattrStatus = status;
}
public void setFlattrStatus(FlattrStatus status) {
this.flattrStatus = status;
}
public FlattrStatus getFlattrStatus() {
return flattrStatus;
}
public FlattrStatus getFlattrStatus() {
return flattrStatus;
}
public String getPaymentLink() {
return paymentLink;
@ -442,4 +479,28 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource
return null;
}
}
public int getPageNr() {
return pageNr;
}
public void setPageNr(int pageNr) {
this.pageNr = pageNr;
}
public boolean isPaged() {
return paged;
}
public void setPaged(boolean paged) {
this.paged = paged;
}
public String getNextPageLink() {
return nextPageLink;
}
public void setNextPageLink(String nextPageLink) {
this.nextPageLink = nextPageLink;
}
}

View File

@ -1,5 +1,6 @@
package de.danoeh.antennapod.core.service.download;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@ -15,6 +16,7 @@ public class DownloadRequest implements Parcelable {
private boolean deleteOnFailure;
private final long feedfileId;
private final int feedfileType;
private final Bundle arguments;
protected int progressPercent;
protected long soFar;
@ -22,7 +24,7 @@ public class DownloadRequest implements Parcelable {
protected int statusMsg;
public DownloadRequest(String destination, String source, String title,
long feedfileId, int feedfileType, String username, String password, boolean deleteOnFailure) {
long feedfileId, int feedfileType, String username, String password, boolean deleteOnFailure, Bundle arguments) {
Validate.notNull(destination);
Validate.notNull(source);
Validate.notNull(title);
@ -35,11 +37,12 @@ public class DownloadRequest implements Parcelable {
this.username = username;
this.password = password;
this.deleteOnFailure = deleteOnFailure;
this.arguments = (arguments != null) ? arguments : new Bundle();
}
public DownloadRequest(String destination, String source, String title,
long feedfileId, int feedfileType) {
this(destination, source, title, feedfileId, feedfileType, null, null, true);
this(destination, source, title, feedfileId, feedfileType, null, null, true, null);
}
private DownloadRequest(Parcel in) {
@ -49,6 +52,7 @@ public class DownloadRequest implements Parcelable {
feedfileId = in.readLong();
feedfileType = in.readInt();
deleteOnFailure = (in.readByte() > 0);
arguments = in.readBundle();
if (in.dataAvail() > 0) {
username = in.readString();
} else {
@ -74,6 +78,7 @@ public class DownloadRequest implements Parcelable {
dest.writeLong(feedfileId);
dest.writeInt(feedfileType);
dest.writeByte((deleteOnFailure) ? (byte) 1 : 0);
dest.writeBundle(arguments);
if (username != null) {
dest.writeString(username);
}
@ -92,6 +97,7 @@ public class DownloadRequest implements Parcelable {
}
};
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -106,11 +112,11 @@ public class DownloadRequest implements Parcelable {
if (size != that.size) return false;
if (soFar != that.soFar) return false;
if (statusMsg != that.statusMsg) return false;
if (destination != null ? !destination.equals(that.destination) : that.destination != null)
return false;
if (!arguments.equals(that.arguments)) return false;
if (!destination.equals(that.destination)) return false;
if (password != null ? !password.equals(that.password) : that.password != null)
return false;
if (source != null ? !source.equals(that.source) : that.source != null) return false;
if (!source.equals(that.source)) return false;
if (title != null ? !title.equals(that.title) : that.title != null) return false;
if (username != null ? !username.equals(that.username) : that.username != null)
return false;
@ -120,14 +126,15 @@ public class DownloadRequest implements Parcelable {
@Override
public int hashCode() {
int result = destination != null ? destination.hashCode() : 0;
result = 31 * result + (source != null ? source.hashCode() : 0);
int result = destination.hashCode();
result = 31 * result + source.hashCode();
result = 31 * result + (title != null ? title.hashCode() : 0);
result = 31 * result + (username != null ? username.hashCode() : 0);
result = 31 * result + (password != null ? password.hashCode() : 0);
result = 31 * result + (deleteOnFailure ? 1 : 0);
result = 31 * result + (int) (feedfileId ^ (feedfileId >>> 32));
result = 31 * result + feedfileType;
result = 31 * result + arguments.hashCode();
result = 31 * result + progressPercent;
result = 31 * result + (int) (soFar ^ (soFar >>> 32));
result = 31 * result + (int) (size ^ (size >>> 32));
@ -206,4 +213,8 @@ public class DownloadRequest implements Parcelable {
public boolean isDeleteOnFailure() {
return deleteOnFailure;
}
public Bundle getArguments() {
return arguments;
}
}

View File

@ -15,6 +15,7 @@ import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v4.util.Pair;
import android.util.Log;
import android.webkit.URLUtil;
@ -66,6 +67,7 @@ import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.syndication.handler.FeedHandler;
import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult;
import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.DownloadError;
@ -669,7 +671,7 @@ public class DownloadService extends Service {
private static final String TAG = "FeedSyncThread";
private BlockingQueue<DownloadRequest> completedRequests = new LinkedBlockingDeque<DownloadRequest>();
private CompletionService<Feed> parserService = new ExecutorCompletionService<Feed>(Executors.newSingleThreadExecutor());
private CompletionService<Pair<DownloadRequest, FeedHandlerResult>> parserService = new ExecutorCompletionService<Pair<DownloadRequest, FeedHandlerResult>>(Executors.newSingleThreadExecutor());
private ExecutorService dbService = Executors.newSingleThreadExecutor();
private Future<?> dbUpdateFuture;
private volatile boolean isActive = true;
@ -684,8 +686,8 @@ public class DownloadService extends Service {
*
* @return Collected feeds or null if the method has been interrupted during the first waiting period.
*/
private List<Feed> collectCompletedRequests() {
List<Feed> results = new LinkedList<Feed>();
private List<Pair<DownloadRequest, FeedHandlerResult>> collectCompletedRequests() {
List<Pair<DownloadRequest, FeedHandlerResult>> results = new LinkedList<Pair<DownloadRequest, FeedHandlerResult>>();
DownloadRequester requester = DownloadRequester.getInstance();
int tasks = 0;
@ -727,9 +729,9 @@ public class DownloadService extends Service {
for (int i = 0; i < tasks; i++) {
try {
Feed f = parserService.take().get();
if (f != null) {
results.add(f);
Pair<DownloadRequest, FeedHandlerResult> result = parserService.take().get();
if (result != null) {
results.add(result);
}
} catch (InterruptedException e) {
e.printStackTrace();
@ -754,16 +756,16 @@ public class DownloadService extends Service {
@Override
public void run() {
while (isActive) {
final List<Feed> feeds = collectCompletedRequests();
final List<Pair<DownloadRequest, FeedHandlerResult>> results = collectCompletedRequests();
if (feeds == null) {
if (results == null) {
continue;
}
if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + feeds.size() + " feeds");
if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + results.size() + " feeds");
for (Feed feed : feeds) {
removeDuplicateImages(feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet.
for (Pair<DownloadRequest, FeedHandlerResult> result : results) {
removeDuplicateImages(result.second.feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet.
}
// Save information of feed in DB
@ -780,9 +782,10 @@ public class DownloadService extends Service {
dbUpdateFuture = dbService.submit(new Runnable() {
@Override
public void run() {
Feed[] savedFeeds = DBTasks.updateFeed(DownloadService.this, feeds.toArray(new Feed[feeds.size()]));
Feed[] savedFeeds = DBTasks.updateFeed(DownloadService.this, getFeeds(results));
for (Feed savedFeed : savedFeeds) {
for (int i = 0; i < savedFeeds.length; i++) {
Feed savedFeed = savedFeeds[i];
// Download Feed Image if provided and not downloaded
if (savedFeed.getImage() != null
&& savedFeed.getImage().isDownloaded() == false) {
@ -816,6 +819,19 @@ public class DownloadService extends Service {
}
}
// If loadAllPages=true, check if another page is available and queue it for download
final boolean loadAllPages = results.get(i).first.getArguments().getBoolean(DownloadRequester.REQUEST_ARG_LOAD_ALL_PAGES);
final Feed feed = results.get(i).second.feed;
if (loadAllPages && feed.getNextPageLink() != null) {
try {
feed.setId(savedFeed.getId());
DBTasks.loadNextPageOfFeed(DownloadService.this, savedFeed, true);
} catch (DownloadRequestException e) {
Log.e(TAG, "Error trying to load next page", e);
}
}
ClientConfig.downloadServiceCallbacks.onFeedParsed(DownloadService.this,
savedFeed);
@ -840,12 +856,22 @@ public class DownloadService extends Service {
}
if (BuildConfig.DEBUG) Log.d(TAG, "Shutting down");
}
private class FeedParserTask implements Callable<Feed> {
/**
* Helper method
*/
private Feed[] getFeeds(List<Pair<DownloadRequest, FeedHandlerResult>> results) {
Feed[] feeds = new Feed[results.size()];
for (int i = 0; i < results.size(); i++) {
feeds[i] = results.get(i).second.feed;
}
return feeds;
}
private class FeedParserTask implements Callable<Pair<DownloadRequest, FeedHandlerResult>> {
private DownloadRequest request;
@ -854,27 +880,28 @@ public class DownloadService extends Service {
}
@Override
public Feed call() throws Exception {
public Pair<DownloadRequest, FeedHandlerResult> call() throws Exception {
return parseFeed(request);
}
}
private Feed parseFeed(DownloadRequest request) {
Feed savedFeed = null;
private Pair<DownloadRequest, FeedHandlerResult> parseFeed(DownloadRequest request) {
Feed feed = new Feed(request.getSource(), new Date());
feed.setFile_url(request.getDestination());
feed.setId(request.getFeedfileId());
feed.setDownloaded(true);
feed.setPreferences(new FeedPreferences(0, true, request.getUsername(), request.getPassword()));
feed.setPreferences(new FeedPreferences(0, true,
request.getUsername(), request.getPassword()));
feed.setPageNr(request.getArguments().getInt(DownloadRequester.REQUEST_ARG_PAGE_NR, 0));
DownloadError reason = null;
String reasonDetailed = null;
boolean successful = true;
FeedHandler feedHandler = new FeedHandler();
FeedHandlerResult result = null;
try {
feed = feedHandler.parseFeed(feed).feed;
result = feedHandler.parseFeed(feed);
if (BuildConfig.DEBUG)
Log.d(TAG, feed.getTitle() + " parsed");
if (checkFeedData(feed) == false) {
@ -909,17 +936,14 @@ public class DownloadService extends Service {
}
// cleanup();
if (savedFeed == null) {
savedFeed = feed;
}
if (successful) {
return savedFeed;
return Pair.create(request, result);
} else {
numberOfDownloads.decrementAndGet();
saveDownloadStatus(new DownloadStatus(savedFeed,
savedFeed.getHumanReadableIdentifier(), reason, successful,
saveDownloadStatus(new DownloadStatus(feed,
feed.getHumanReadableIdentifier(), reason, successful,
reasonDetailed));
return null;
}

View File

@ -350,7 +350,9 @@ public final class DBReader {
cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FILE_URL),
cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOAD_URL),
cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0,
new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS)));
new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS)),
cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_IS_PAGED) > 0,
cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_NEXT_PAGE_LINK));
if (image != null) {
image.setOwner(feed);

View File

@ -4,11 +4,32 @@ import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.asynctask.FlattrClickWorker;
import de.danoeh.antennapod.core.asynctask.FlattrStatusFetcher;
import de.danoeh.antennapod.core.feed.*;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
@ -20,10 +41,6 @@ import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException;
import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Provides methods for doing common tasks that use DBReader and DBWriter.
*/
@ -239,6 +256,49 @@ public final class DBTasks {
}
/**
* Downloads all pages of the given feed.
*
* @param context Used for requesting the download.
* @param feed The Feed object.
*/
public static void refreshCompleteFeed(final Context context, final Feed feed) {
try {
refreshFeed(context, feed, true);
} catch (DownloadRequestException e) {
e.printStackTrace();
DBWriter.addDownloadStatus(
context,
new DownloadStatus(feed, feed
.getHumanReadableIdentifier(),
DownloadError.ERROR_REQUEST_ERROR, false, e
.getMessage()
)
);
}
}
/**
* Queues the next page of this Feed for download. The given Feed has to be a paged
* Feed (isPaged()=true) and must contain a nextPageLink.
*
* @param context Used for requesting the download.
* @param feed The feed whose next page should be loaded.
* @param loadAllPages True if any subsequent pages should also be loaded, false otherwise.
*/
public static void loadNextPageOfFeed(final Context context, Feed feed, boolean loadAllPages) throws DownloadRequestException {
if (feed.isPaged() && feed.getNextPageLink() != null) {
int pageNr = feed.getPageNr() + 1;
Feed nextFeed = new Feed(feed.getNextPageLink(), new Date(), feed.getTitle() + "(" + pageNr + ")");
nextFeed.setPageNr(pageNr);
nextFeed.setPaged(true);
nextFeed.setId(feed.getId());
DownloadRequester.getInstance().downloadFeed(context, nextFeed, loadAllPages);
} else {
Log.e(TAG, "loadNextPageOfFeed: Feed was either not paged or contained no nextPageLink");
}
}
/**
* Updates a specific Feed.
*
@ -247,6 +307,10 @@ public final class DBTasks {
*/
public static void refreshFeed(Context context, Feed feed)
throws DownloadRequestException {
refreshFeed(context, feed, false);
}
private static void refreshFeed(Context context, Feed feed, boolean loadAllPages) throws DownloadRequestException {
Feed f;
if (feed.getPreferences() == null) {
f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle());
@ -255,7 +319,7 @@ public final class DBTasks {
feed.getPreferences().getUsername(), feed.getPreferences().getPassword());
}
f.setId(feed.getId());
DownloadRequester.getInstance().downloadFeed(context, f);
DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages);
}
/**
@ -388,7 +452,7 @@ public final class DBTasks {
* 2. There is free space in the episode cache
* This method is executed on an internal single thread executor.
*
* @param context Used for accessing the DB.
* @param context Used for accessing the DB.
* @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if
* its media ID is in the mediaIds list.
* @return A Future that can be used for waiting for the methods completion.
@ -687,11 +751,21 @@ public final class DBTasks {
+ " already exists. Syncing new with existing one.");
Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
if (savedFeed.compareWithOther(newFeed)) {
final boolean markNewItemsAsUnread;
if (newFeed.getPageNr() == savedFeed.getPageNr()) {
if (savedFeed.compareWithOther(newFeed)) {
if (BuildConfig.DEBUG)
Log.d(TAG,
"Feed has updated attribute values. Updating old feed's attributes");
savedFeed.updateFromOther(newFeed);
}
markNewItemsAsUnread = true;
} else {
if (BuildConfig.DEBUG)
Log.d(TAG,
"Feed has updated attribute values. Updating old feed's attributes");
savedFeed.updateFromOther(newFeed);
Log.d(TAG, "New feed has a higher page number. Merging without marking as unread");
markNewItemsAsUnread = false;
savedFeed.setNextPageLink(newFeed.getNextPageLink());
}
if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) {
if (BuildConfig.DEBUG)
@ -708,7 +782,9 @@ public final class DBTasks {
final int i = idx;
item.setFeed(savedFeed);
savedFeed.getItems().add(i, item);
item.setRead(false);
if (markNewItemsAsUnread) {
item.setRead(false);
}
} else {
oldItem.updateFromOther(item);
}

View File

@ -2,15 +2,10 @@ package de.danoeh.antennapod.core.storage;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.webkit.URLUtil;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.*;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.util.FileNameGenerator;
import de.danoeh.antennapod.core.util.URLChecker;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
@ -19,6 +14,18 @@ import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedFile;
import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.util.FileNameGenerator;
import de.danoeh.antennapod.core.util.URLChecker;
/**
* Sends download requests to the DownloadService. This class should always be used for starting downloads,
@ -31,6 +38,16 @@ public class DownloadRequester {
public static final String FEED_DOWNLOADPATH = "cache/";
public static final String MEDIA_DOWNLOADPATH = "media/";
/**
* Denotes the page of the feed that is contained in the DownloadRequest sent by the DownloadRequester.
*/
public static final String REQUEST_ARG_PAGE_NR = "page";
/**
* True if all pages after the feed that is contained in this DownloadRequest should be downloaded.
*/
public static final String REQUEST_ARG_LOAD_ALL_PAGES = "loadAllPages";
private static DownloadRequester downloader;
private Map<String, DownloadRequest> downloads;
@ -74,7 +91,7 @@ public class DownloadRequester {
}
private void download(Context context, FeedFile item, File dest,
boolean overwriteIfExists, String username, String password, boolean deleteOnFailure) {
boolean overwriteIfExists, String username, String password, boolean deleteOnFailure, Bundle arguments) {
if (!isDownloadingFile(item)) {
if (!isFilenameAvailable(dest.toString()) || (deleteOnFailure && dest.exists())) {
if (BuildConfig.DEBUG)
@ -116,7 +133,7 @@ public class DownloadRequester {
DownloadRequest request = new DownloadRequest(dest.toString(),
URLChecker.prepareURL(item.getDownload_url()), item.getHumanReadableIdentifier(),
item.getId(), item.getTypeAsInt(), username, password, deleteOnFailure);
item.getId(), item.getTypeAsInt(), username, password, deleteOnFailure, arguments);
download(context, request);
} else {
@ -144,22 +161,30 @@ public class DownloadRequester {
return true;
}
public synchronized void downloadFeed(Context context, Feed feed)
public synchronized void downloadFeed(Context context, Feed feed, boolean loadAllPages)
throws DownloadRequestException {
if (feedFileValid(feed)) {
String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null;
String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null;
Bundle args = new Bundle();
args.putInt(REQUEST_ARG_PAGE_NR, feed.getPageNr());
args.putBoolean(REQUEST_ARG_LOAD_ALL_PAGES, loadAllPages);
download(context, feed, new File(getFeedfilePath(context),
getFeedfileName(feed)), true, username, password, true);
getFeedfileName(feed)), true, username, password, true, args);
}
}
public synchronized void downloadFeed(Context context, Feed feed) throws DownloadRequestException {
downloadFeed(context, feed, false);
}
public synchronized void downloadImage(Context context, FeedImage image)
throws DownloadRequestException {
if (feedFileValid(image)) {
download(context, image, new File(getImagefilePath(context),
getImagefileName(image)), false, null, null, false);
getImagefileName(image)), false, null, null, false, null);
}
}
@ -185,8 +210,7 @@ public class DownloadRequester {
getMediafilename(feedmedia));
}
download(context, feedmedia,
dest, false, username, password, false
);
dest, false, username, password, false, null);
}
}

View File

@ -67,6 +67,9 @@ public class PodDBAdapter {
public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14;
public static final int KEY_FEED_USERNAME_INDEX = 15;
public static final int KEY_FEED_PASSWORD_INDEX = 16;
public static final int KEY_IS_PAGED_INDEX = 17;
public static final int KEY_LOAD_ALL_PAGES_INDEX = 18;
public static final int KEY_NEXT_PAGE_LINK_INDEX = 19;
// ----------- FeedItem indices
public static final int KEY_CONTENT_ENCODED_INDEX = 2;
public static final int KEY_PUBDATE_INDEX = 3;
@ -144,6 +147,8 @@ public class PodDBAdapter {
public static final String KEY_PLAYED_DURATION = "played_duration";
public static final String KEY_USERNAME = "username";
public static final String KEY_PASSWORD = "password";
public static final String KEY_IS_PAGED = "is_paged";
public static final String KEY_NEXT_PAGE_LINK = "next_page_link";
// Table names
public static final String TABLE_NAME_FEEDS = "Feeds";
@ -168,7 +173,10 @@ public class PodDBAdapter {
+ KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1,"
+ KEY_FLATTR_STATUS + " INTEGER,"
+ KEY_USERNAME + " TEXT,"
+ KEY_PASSWORD + " TEXT)";
+ KEY_PASSWORD + " TEXT,"
+ KEY_IS_PAGED + " INTEGER DEFAULT 0,"
+ KEY_NEXT_PAGE_LINK + " TEXT)";
public static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE "
+ TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
@ -234,8 +242,10 @@ public class PodDBAdapter {
TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER,
TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD,
TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS,
TABLE_NAME_FEEDS + "." + KEY_IS_PAGED,
TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK,
TABLE_NAME_FEEDS + "." + KEY_USERNAME,
TABLE_NAME_FEEDS + "." + KEY_PASSWORD
TABLE_NAME_FEEDS + "." + KEY_PASSWORD,
};
// column indices for FEED_SEL_STD
@ -255,8 +265,10 @@ public class PodDBAdapter {
public static final int IDX_FEED_SEL_STD_FEED_IDENTIFIER = 13;
public static final int IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD = 14;
public static final int IDX_FEED_SEL_STD_FLATTR_STATUS = 15;
public static final int IDX_FEED_SEL_PREFERENCES_USERNAME = 16;
public static final int IDX_FEED_SEL_PREFERENCES_PASSWORD = 17;
public static final int IDX_FEED_SEL_STD_IS_PAGED = 16;
public static final int IDX_FEED_SEL_STD_NEXT_PAGE_LINK = 17;
public static final int IDX_FEED_SEL_PREFERENCES_USERNAME = 18;
public static final int IDX_FEED_SEL_PREFERENCES_PASSWORD = 19;
/**
@ -386,6 +398,8 @@ public class PodDBAdapter {
Log.d(TAG, "Setting feed with flattr status " + feed.getTitle() + ": " + feed.getFlattrStatus().toLong());
values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong());
values.put(KEY_IS_PAGED, feed.isPaged());
values.put(KEY_NEXT_PAGE_LINK, feed.getNextPageLink());
if (feed.getId() == 0) {
// Create new entry
if (BuildConfig.DEBUG)

View File

@ -1,9 +1,9 @@
package de.danoeh.antennapod.core.syndication.handler;
import de.danoeh.antennapod.core.feed.Feed;
import java.util.Map;
import de.danoeh.antennapod.core.feed.Feed;
/**
* Container for results returned by the Feed parser
*/

View File

@ -1,6 +1,9 @@
package de.danoeh.antennapod.core.syndication.namespace.atom;
import android.util.Log;
import org.xml.sax.Attributes;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.feed.FeedItem;
@ -12,7 +15,6 @@ import de.danoeh.antennapod.core.syndication.namespace.Namespace;
import de.danoeh.antennapod.core.syndication.namespace.SyndElement;
import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils;
import org.xml.sax.Attributes;
public class NSAtom extends Namespace {
private static final String TAG = "NSAtom";
@ -44,6 +46,7 @@ public class NSAtom extends Namespace {
private static final String LINK_REL_PAYMENT = "payment";
private static final String LINK_REL_RELATED = "related";
private static final String LINK_REL_SELF = "self";
private static final String LINK_REL_NEXT = "next";
// type-values
private static final String LINK_TYPE_ATOM = "application/atom+xml";
private static final String LINK_TYPE_HTML = "text/html";
@ -120,6 +123,9 @@ public class NSAtom extends Namespace {
}
} else if (rel.equals(LINK_REL_PAYMENT)) {
state.getFeed().setPaymentLink(href);
} else if (rel.equals(LINK_REL_NEXT)) {
state.getFeed().setPaged(true);
state.getFeed().setNextPageLink(href);
}
}
}

View File

@ -0,0 +1,56 @@
package de.danoeh.antennapod.core.util.gui;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import de.danoeh.antennapod.core.R;
/**
* Utility methods for the more_content_list_footer layout.
*/
public class MoreContentListFooterUtil {
private final View root;
private boolean loading;
private Listener listener;
public MoreContentListFooterUtil(View root) {
this.root = root;
root.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null && !loading) {
listener.onClick();
}
}
});
}
public void setLoadingState(boolean newState) {
final ImageView imageView = (ImageView) root.findViewById(R.id.imgExpand);
final ProgressBar progressBar = (ProgressBar) root.findViewById(R.id.progBar);
if (newState) {
imageView.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
} else {
imageView.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
}
loading = newState;
}
public void setClickListener(Listener l) {
listener = l;
}
public static interface Listener {
public void onClick();
}
public View getRoot() {
return root;
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/imgExpand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="16dp"
android:contentDescription="@string/load_next_page_label"
android:src="?attr/navigation_expand" />
<ProgressBar
android:id="@+id/progBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="16dp"
android:indeterminateOnly="true"
android:visibility="gone" />
</FrameLayout>

View File

@ -85,6 +85,7 @@
<string name="share_source_label">Share feed link</string>
<string name="feed_delete_confirmation_msg">Please confirm that you want to delete this feed and ALL episodes of this feed that you have downloaded.</string>
<string name="feed_remover_msg">Removing feed</string>
<string name="load_complete_feed">Load complete feed</string>
<!-- actions on feeditems -->
<string name="download_label">Download</string>
@ -369,6 +370,7 @@
<string name="new_episodes_count_label">Number of new episodes</string>
<string name="in_progress_episodes_count_label">Number of episodes you have started listening to</string>
<string name="drag_handle_content_description">Drag to change the position of this item</string>
<string name="load_next_page_label">Load next page</string>
<!-- Feed information screen -->
<string name="authentication_label">Authentication</string>