diff options
Diffstat (limited to 'core')
5 files changed, 167 insertions, 75 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index 7c01b7fef..d5a1007af 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -337,7 +337,10 @@ public class UserPreferences { } - + /* + * Returns update interval in milliseconds; value 0 means that auto update is disabled + * or feeds are updated at a certain time of day + */ public static long getUpdateInterval() { String updateInterval = prefs.getString(PREF_UPDATE_INTERVAL, "0"); if(!updateInterval.contains(":")) { @@ -759,12 +762,12 @@ public class UserPreferences { if (timeOfDay.length == 2) { restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]); } else { - long hours = getUpdateInterval(); - long startTrigger = hours; + long milliseconds = getUpdateInterval(); + long startTrigger = milliseconds; if (now) { startTrigger = TimeUnit.SECONDS.toMillis(10); } - restartUpdateIntervalAlarm(startTrigger, hours); + restartUpdateIntervalAlarm(startTrigger, milliseconds); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index 869cb8f7a..99456b756 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -2,13 +2,21 @@ package de.danoeh.antennapod.core.service.download; import android.text.TextUtils; import android.util.Log; - +import com.squareup.okhttp.Interceptor; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Protocol; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; - +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.core.util.URIUtil; +import okio.ByteString; import org.apache.commons.io.IOUtils; import java.io.BufferedInputStream; @@ -24,16 +32,6 @@ import java.net.UnknownHostException; import java.util.Collections; import java.util.Date; -import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.feed.FeedImage; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.util.DateUtils; -import de.danoeh.antennapod.core.util.DownloadError; -import de.danoeh.antennapod.core.util.StorageUtils; -import de.danoeh.antennapod.core.util.URIUtil; -import okio.ByteString; - public class HttpDownloader extends Downloader { private static final String TAG = "HttpDownloader"; @@ -59,7 +57,8 @@ public class HttpDownloader extends Downloader { } } - OkHttpClient httpClient = AntennapodHttpClient.getHttpClient(); + OkHttpClient httpClient = AntennapodHttpClient.newHttpClient(); + httpClient.interceptors().add(new BasicAuthorizationInterceptor(request)); RandomAccessFile out = null; InputStream connection; ResponseBody responseBody = null; @@ -68,16 +67,16 @@ public class HttpDownloader extends Downloader { final URI uri = URIUtil.getURIFromRequestUrl(request.getSource()); Request.Builder httpReq = new Request.Builder().url(uri.toURL()) .header("User-Agent", ClientConfig.USER_AGENT); - if(request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { // set header explicitly so that okhttp doesn't do transparent gzip Log.d(TAG, "addHeader(\"Accept-Encoding\", \"identity\")"); httpReq.addHeader("Accept-Encoding", "identity"); } - if(!TextUtils.isEmpty(request.getLastModified())) { + if (!TextUtils.isEmpty(request.getLastModified())) { String lastModified = request.getLastModified(); Date lastModifiedDate = DateUtils.parse(lastModified); - if(lastModifiedDate != null) { + if (lastModifiedDate != null) { long threeDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 3; if (lastModifiedDate.getTime() > threeDaysAgo) { Log.d(TAG, "addHeader(\"If-Modified-Since\", \"" + lastModified + "\")"); @@ -89,19 +88,6 @@ public class HttpDownloader extends Downloader { } } - // add authentication information - String userInfo = uri.getUserInfo(); - if (userInfo != null) { - String[] parts = userInfo.split(":"); - if (parts.length == 2) { - String credentials = encodeCredentials(parts[0], parts[1], "ISO-8859-1"); - httpReq.header("Authorization", credentials); - } - } else if (!TextUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { - String credentials = encodeCredentials(request.getUsername(), request.getPassword(), - "ISO-8859-1"); - httpReq.header("Authorization", credentials); - } // add range header if necessary if (fileExists) { @@ -111,15 +97,15 @@ public class HttpDownloader extends Downloader { } Response response; + try { response = httpClient.newCall(httpReq.build()).execute(); - } catch(IOException e) { + } catch (IOException e) { Log.e(TAG, e.toString()); - if(e.getMessage().contains("PROTOCOL_ERROR")) { + if (e.getMessage().contains("PROTOCOL_ERROR")) { httpClient.setProtocols(Collections.singletonList(Protocol.HTTP_1_1)); response = httpClient.newCall(httpReq.build()).execute(); - } - else { + } else { throw e; } } @@ -127,34 +113,13 @@ public class HttpDownloader extends Downloader { responseBody = response.body(); String contentEncodingHeader = response.header("Content-Encoding"); boolean isGzip = false; - if(!TextUtils.isEmpty(contentEncodingHeader)) { + if (!TextUtils.isEmpty(contentEncodingHeader)) { isGzip = TextUtils.equals(contentEncodingHeader.toLowerCase(), "gzip"); } Log.d(TAG, "Response code is " + response.code()); - if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { - Log.d(TAG, "Authorization failed, re-trying with UTF-8 encoding"); - if (userInfo != null) { - String[] parts = userInfo.split(":"); - if (parts.length == 2) { - String credentials = encodeCredentials(parts[0], parts[1], "UTF-8"); - httpReq.header("Authorization", credentials); - } - } else if (!TextUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { - String credentials = encodeCredentials(request.getUsername(), request.getPassword(), - "UTF-8"); - httpReq.header("Authorization", credentials); - } - response = httpClient.newCall(httpReq.build()).execute(); - responseBody = response.body(); - contentEncodingHeader = response.header("Content-Encoding"); - if(!TextUtils.isEmpty(contentEncodingHeader)) { - isGzip = TextUtils.equals(contentEncodingHeader.toLowerCase(), "gzip"); - } - } - - if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { + if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { Log.d(TAG, "Feed '" + request.getSource() + "' not modified since last update, Download canceled"); onCancelled(); return; @@ -166,7 +131,7 @@ public class HttpDownloader extends Downloader { if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { error = DownloadError.ERROR_UNAUTHORIZED; details = String.valueOf(response.code()); - } else if(response.code() == HttpURLConnection.HTTP_FORBIDDEN) { + } else if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) { error = DownloadError.ERROR_FORBIDDEN; details = String.valueOf(response.code()); } else { @@ -184,18 +149,19 @@ public class HttpDownloader extends Downloader { // fail with a file type error when the content type is text and // the reported content length is less than 100kb (or no length is given) - if(request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { int contentLength = -1; String contentLen = response.header("Content-Length"); - if(contentLen != null) { + if (contentLen != null) { try { contentLength = Integer.parseInt(contentLen); - } catch(NumberFormatException e) {} + } catch (NumberFormatException e) { + } } Log.d(TAG, "content length: " + contentLength); String contentType = response.header("Content-Type"); Log.d(TAG, "content type: " + contentType); - if(contentType != null && contentType.startsWith("text/") && + if (contentType != null && contentType.startsWith("text/") && contentLength < 100 * 1024) { onFail(DownloadError.ERROR_FILE_TYPE, null); return; @@ -244,10 +210,10 @@ public class HttpDownloader extends Downloader { while (!cancelled && (count = connection.read(buffer)) != -1) { out.write(buffer, 0, count); request.setSoFar(request.getSoFar() + count); - int progressPercent = (int)(100.0 * request.getSoFar() / request.getSize()); + int progressPercent = (int) (100.0 * request.getSoFar() / request.getSize()); request.setProgressPercent(progressPercent); } - } catch(IOException e) { + } catch (IOException e) { Log.e(TAG, Log.getStackTraceString(e)); } if (cancelled) { @@ -260,12 +226,12 @@ public class HttpDownloader extends Downloader { onFail(DownloadError.ERROR_IO_ERROR, "Download completed but size: " + request.getSoFar() + " does not equal expected size " + request.getSize()); return; - } else if(request.getSize() > 0 && request.getSoFar() == 0){ + } else if (request.getSize() > 0 && request.getSoFar() == 0) { onFail(DownloadError.ERROR_IO_ERROR, "Download completed, but nothing was read"); return; } String lastModified = response.header("Last-Modified"); - if(lastModified != null) { + if (lastModified != null) { request.setLastModified(lastModified); } else { request.setLastModified(response.header("ETag")); @@ -325,7 +291,7 @@ public class HttpDownloader extends Downloader { if (dest.exists()) { boolean rc = dest.delete(); Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " - + rc); + + rc); } else { Log.d(TAG, "cleanup() didn't delete file: does not exist."); } @@ -343,4 +309,62 @@ public class HttpDownloader extends Downloader { } } + private class BasicAuthorizationInterceptor implements Interceptor { + + private DownloadRequest downloadRequest; + + public BasicAuthorizationInterceptor(DownloadRequest downloadRequest) { + this.downloadRequest = downloadRequest; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + String userInfo = URIUtil.getURIFromRequestUrl(downloadRequest.getSource()).getUserInfo(); + + Response response = chain.proceed(request); + + if (response.code() != HttpURLConnection.HTTP_UNAUTHORIZED) { + return response; + } + + Request.Builder newRequest = request.newBuilder(); + + Log.d(TAG, "Authorization failed, re-trying with ISO-8859-1 encoded credentials"); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + if (parts.length == 2) { + String credentials = encodeCredentials(parts[0], parts[1], "ISO-8859-1"); + newRequest.header("Authorization", credentials); + } + } else if (!TextUtils.isEmpty(downloadRequest.getUsername()) && downloadRequest.getPassword() != null) { + String credentials = encodeCredentials(downloadRequest.getUsername(), downloadRequest.getPassword(), + "ISO-8859-1"); + newRequest.header("Authorization", credentials); + } + + response = chain.proceed(newRequest.build()); + + if (response.code() != HttpURLConnection.HTTP_UNAUTHORIZED) { + return response; + } + + Log.d(TAG, "Authorization failed, re-trying with UTF-8 encoded credentials"); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + if (parts.length == 2) { + String credentials = encodeCredentials(parts[0], parts[1], "UTF-8"); + newRequest.header("Authorization", credentials); + } + } else if (!TextUtils.isEmpty(downloadRequest.getUsername()) && downloadRequest.getPassword() != null) { + String credentials = encodeCredentials(downloadRequest.getUsername(), downloadRequest.getPassword(), + "UTF-8"); + newRequest.header("Authorization", credentials); + } + + return chain.proceed(newRequest.build()); + } + + } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index 15656de3b..e67dc9d0a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -1004,13 +1004,37 @@ public class PlaybackService extends MediaBrowserServiceCompat { state = PlaybackStateCompat.STATE_NONE; } sessionState.setState(state, mediaPlayer.getPosition(), mediaPlayer.getPlaybackSpeed()); - sessionState.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE + long capabilities = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT); + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT; + + if (useSkipToPreviousForRewindInLockscreen()) { + // Workaround to fool Android so that Lockscreen will expose a skip-to-previous button, + // which will be used for rewind. + // The workaround is used for pre Lollipop (Androidv5) devices. + // For Androidv5+, lockscreen widges are really notifications (compact), + // with an independent codepath + // + // @see #sessionCallback in the backing callback, skipToPrevious implementation + // is actually the same as rewind. So no new inconsistency is created. + // @see #setupNotification() for the method to create Androidv5+ lockscreen UI + // with notification (compact) + capabilities = capabilities | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + } + + sessionState.setActions(capabilities); mediaSession.setPlaybackState(sessionState.build()); } + private static boolean useSkipToPreviousForRewindInLockscreen() { + // showRewindOnCompactNotification() corresponds to the "Set Lockscreen Buttons" + // Settings in UI. + // Hence, from user perspective, he/she is setting the buttons for Lockscreen + return ( UserPreferences.showRewindOnCompactNotification() && + (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) ); + } + /** * Used by updateMediaSessionMetadata to load notification data in another thread. */ diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index d17b815ff..4d442aac2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.storage; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.database.Cursor; import android.util.Log; @@ -16,7 +17,7 @@ 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.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import de.danoeh.antennapod.core.ClientConfig; @@ -27,21 +28,29 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.GpodnetSyncService; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.LongList; 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 static android.content.Context.MODE_PRIVATE; +import static android.provider.Contacts.SettingsColumns.KEY; + /** * Provides methods for doing common tasks that use DBReader and DBWriter. */ public final class DBTasks { private static final String TAG = "DBTasks"; + public static final String PREF_NAME = "dbtasks"; + private static final String PREF_LAST_REFRESH = "last_refresh"; + /** * Executor service used by the autodownloadUndownloadedEpisodes method. */ @@ -162,6 +171,9 @@ public final class DBTasks { } isRefreshing.set(false); + SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE); + prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply(); + if (FlattrUtils.hasToken()) { Log.d(TAG, "Flattring all pending things."); new FlattrClickWorker(context).executeAsync(); // flattr pending things @@ -313,6 +325,31 @@ public final class DBTasks { DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force); } + /* + * Checks if the app should refresh all feeds, i.e. if the last auto refresh failed. + * + * The feeds are only refreshed if an update interval or time of day is set and the last + * (successful) refresh was before the last interval or more than a day ago, respectively. + */ + public static void checkShouldRefreshFeeds(Context context) { + long interval = 0; + if(UserPreferences.getUpdateInterval() > 0) { + interval = UserPreferences.getUpdateInterval(); + } else if(UserPreferences.getUpdateTimeOfDay().length > 0){ + interval = TimeUnit.DAYS.toMillis(1); + } + if(interval == 0) { // auto refresh is disabled + return; + } + SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE); + long lastRefresh = prefs.getLong(PREF_LAST_REFRESH, 0); + Log.d(TAG, "last refresh: " + Converter.getDurationStringLocalized(context, + System.currentTimeMillis() - lastRefresh) + " ago"); + if(lastRefresh <= System.currentTimeMillis() - interval) { + DBTasks.refreshAllFeeds(context, null); + } + } + /** * Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the * DB and send a FeedUpdateBroadcast. diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java index 52b05aa98..ec7ebe1ac 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java @@ -30,6 +30,7 @@ public class NSAtom extends Namespace { private static final String AUTHOR = "author"; private static final String AUTHOR_NAME = "name"; private static final String CONTENT = "content"; + private static final String SUMMARY = "summary"; private static final String IMAGE_LOGO = "logo"; private static final String IMAGE_ICON = "icon"; private static final String SUBTITLE = "subtitle"; @@ -59,8 +60,8 @@ public class NSAtom extends Namespace { /** * Regexp to test whether an Element is a Text Element. */ - private static final String isText = TITLE + "|" + CONTENT + "|" + "|" - + SUBTITLE; + private static final String isText = TITLE + "|" + CONTENT + "|" + + SUBTITLE + "|" + SUMMARY; public static final String isFeed = FEED + "|" + NSRSS20.CHANNEL; public static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM; @@ -191,6 +192,9 @@ public class NSAtom extends Namespace { } else if (CONTENT.equals(top) && ENTRY.equals(second) && textElement != null && state.getCurrentItem() != null) { state.getCurrentItem().setDescription(textElement.getProcessedContent()); + } else if (SUMMARY.equals(top) && ENTRY.equals(second) && textElement != null && + state.getCurrentItem() != null && state.getCurrentItem().getDescription() == null) { + state.getCurrentItem().setDescription(textElement.getProcessedContent()); } else if (UPDATED.equals(top) && ENTRY.equals(second) && state.getCurrentItem() != null && state.getCurrentItem().getPubDate() == null) { state.getCurrentItem().setPubDate(DateUtils.parse(content)); |