diff options
Diffstat (limited to 'core/src/main/java')
32 files changed, 790 insertions, 357 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java new file mode 100644 index 000000000..f7757935a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java @@ -0,0 +1,6 @@ +package de.danoeh.antennapod.core.event; + +public class DiscoveryDefaultUpdateEvent { + public DiscoveryDefaultUpdateEvent() { + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java index 9c1c55d76..a3b66c951 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java @@ -353,7 +353,7 @@ public class Feed extends FeedFile implements ImageResource { Date mostRecentDate = new Date(0); FeedItem mostRecentItem = null; for (FeedItem item : items) { - if (item.getPubDate().after(mostRecentDate)) { + if (item.getPubDate() != null && item.getPubDate().after(mostRecentDate)) { mostRecentDate = item.getPubDate(); mostRecentItem = item; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java index d34e23506..e8e478a86 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java @@ -129,4 +129,8 @@ public class FeedItemFilter { return mProperties.clone(); } + public boolean isShowDownloaded() { + return showDownloaded; + } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java index 2a2568f28..5ffee0d62 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java @@ -38,11 +38,8 @@ public class FeedPreferences { private int feedSkipEnding; public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction auto_delete_action, VolumeAdaptionSetting volumeAdaptionSetting, String username, String password) { - this(feedID, autoDownload, true, auto_delete_action, volumeAdaptionSetting, username, password, new FeedFilter(), SPEED_USE_GLOBAL); - } - - private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction auto_delete_action, VolumeAdaptionSetting volumeAdaptionSetting, String username, String password, @NonNull FeedFilter filter, float feedPlaybackSpeed) { - this(feedID, autoDownload, true, auto_delete_action, volumeAdaptionSetting, username, password, new FeedFilter(), feedPlaybackSpeed, 0, 0); + this(feedID, autoDownload, true, auto_delete_action, volumeAdaptionSetting, + username, password, new FeedFilter(), SPEED_USE_GLOBAL, 0, 0); } private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction auto_delete_action, VolumeAdaptionSetting volumeAdaptionSetting, String username, String password, @NonNull FeedFilter filter, float feedPlaybackSpeed, int feedSkipIntro, int feedSkipEnding) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java new file mode 100644 index 000000000..93f098ecf --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java @@ -0,0 +1,106 @@ +package de.danoeh.antennapod.core.feed; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.core.util.LongIntMap; + +public class SubscriptionsFilter { + private static final String divider = ","; + + private final String[] properties; + + private boolean showIfCounterGreaterZero = false; + + private boolean showAutoDownloadEnabled = false; + private boolean showAutoDownloadDisabled = false; + + private boolean showUpdatedEnabled = false; + private boolean showUpdatedDisabled = false; + + public SubscriptionsFilter(String properties) { + this(TextUtils.split(properties, divider)); + } + + + public SubscriptionsFilter(String[] properties) { + this.properties = properties; + for (String property : properties) { + // see R.arrays.feed_filter_values + switch (property) { + case "counter_greater_zero": + showIfCounterGreaterZero = true; + break; + case "enabled_auto_download": + showAutoDownloadEnabled = true; + break; + case "disabled_auto_download": + showAutoDownloadDisabled = true; + break; + case "enabled_updates": + showUpdatedEnabled = true; + break; + case "disabled_updates": + showUpdatedDisabled = true; + break; + default: + break; + } + } + } + + public boolean isEnabled() { + return properties.length > 0; + } + + /** + * Run a list of feed items through the filter. + */ + public List<Feed> filter(List<Feed> items, LongIntMap feedCounters) { + if (properties.length == 0) { + return items; + } + + List<Feed> result = new ArrayList<>(); + + for (Feed item : items) { + FeedPreferences itemPreferences = item.getPreferences(); + + // If the item does not meet a requirement, skip it. + if (showAutoDownloadEnabled && !itemPreferences.getAutoDownload()) { + continue; + } else if (showAutoDownloadDisabled && itemPreferences.getAutoDownload()) { + continue; + } + + if (showUpdatedEnabled && !itemPreferences.getKeepUpdated()) { + continue; + } else if (showUpdatedDisabled && itemPreferences.getKeepUpdated()) { + continue; + } + + // If the item reaches here, it meets all criteria (except counter > 0) + result.add(item); + } + + if (showIfCounterGreaterZero) { + for (int i = result.size() - 1; i >= 0; i--) { + if (feedCounters.get(result.get(i).getId()) <= 0) { + result.remove(i); + } + } + } + + return result; + } + + public String[] getValues() { + return properties.clone(); + } + + public String serialize() { + return TextUtils.join(divider, getValues()); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java new file mode 100644 index 000000000..7db0456a0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.core.feed; + +import de.danoeh.antennapod.core.R; + +public enum SubscriptionsFilterGroup { + COUNTER_GREATER_ZERO(new ItemProperties(R.string.subscriptions_counter_greater_zero, "counter_greater_zero")), + AUTO_DOWNLOAD(new ItemProperties(R.string.auto_downloaded, "enabled_auto_download"), + new ItemProperties(R.string.not_auto_downloaded, "disabled_auto_download")), + UPDATED(new ItemProperties(R.string.kept_updated, "enabled_updates"), + new ItemProperties(R.string.not_kept_updated, "disabled_updates")); + + + public final ItemProperties[] values; + + SubscriptionsFilterGroup(ItemProperties... values) { + this.values = values; + } + + public static class ItemProperties { + + public final int displayName; + public final String filterId; + + public ItemProperties(int displayName, String filterId) { + this.displayName = displayName; + this.filterId = filterId; + } + + } +} 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 bcbc041a6..1dc4b6093 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 @@ -32,6 +32,7 @@ import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.service.download.ProxyConfig; import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; @@ -66,7 +67,7 @@ public class UserPreferences { private static final String PREF_SHOW_AUTO_DOWNLOAD_REPORT = "prefShowAutoDownloadReport"; public static final String PREF_BACK_BUTTON_BEHAVIOR = "prefBackButtonBehavior"; private static final String PREF_BACK_BUTTON_GO_TO_PAGE = "prefBackButtonGoToPage"; - public static final String PREF_FILTER_FEED = "prefFeedFilter"; + public static final String PREF_FILTER_FEED = "prefSubscriptionsFilter"; public static final String PREF_QUEUE_KEEP_SORTED = "prefQueueKeepSorted"; public static final String PREF_QUEUE_KEEP_SORTED_ORDER = "prefQueueKeepSortedOrder"; @@ -149,8 +150,6 @@ public class UserPreferences { public static final int FEED_COUNTER_SHOW_UNPLAYED = 2; public static final int FEED_COUNTER_SHOW_NONE = 3; public static final int FEED_COUNTER_SHOW_DOWNLOADED = 4; - public static final int FEED_FILTER_NONE = 0; - public static final int FEED_FILTER_COUNTER_ZERO = 1; private static Context context; private static SharedPreferences prefs; @@ -249,7 +248,7 @@ public class UserPreferences { public static void setFeedOrder(String selected) { prefs.edit() .putString(PREF_DRAWER_FEED_ORDER, selected) - .commit(); + .apply(); } public static int getFeedCounterSetting() { @@ -1046,15 +1045,15 @@ public class UserPreferences { .apply(); } - public static int getFeedFilter() { - String value = prefs.getString(PREF_FILTER_FEED, "" + FEED_FILTER_NONE); - return Integer.parseInt(value); + public static SubscriptionsFilter getSubscriptionsFilter() { + String value = prefs.getString(PREF_FILTER_FEED, ""); + return new SubscriptionsFilter(value); } - public static void setFeedFilter(String value) { + public static void setSubscriptionsFilter(SubscriptionsFilter value) { prefs.edit() - .putString(PREF_FILTER_FEED, value) - .commit(); + .putString(PREF_FILTER_FEED, value.serialize()) + .apply(); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java index b683f849c..abee9d8d3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java @@ -15,6 +15,7 @@ public class MediaButtonReceiver extends BroadcastReceiver { private static final String TAG = "MediaButtonReceiver"; public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.KEYCODE"; public static final String EXTRA_SOURCE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.SOURCE"; + public static final String EXTRA_HARDWAREBUTTON = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.HARDWAREBUTTON"; public static final String NOTIFY_BUTTON_RECEIVER = "de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER"; @@ -30,6 +31,12 @@ public class MediaButtonReceiver extends BroadcastReceiver { Intent serviceIntent = new Intent(context, PlaybackService.class); serviceIntent.putExtra(EXTRA_KEYCODE, event.getKeyCode()); serviceIntent.putExtra(EXTRA_SOURCE, event.getSource()); + //detect if this is a hardware button press + if (event.getEventTime() > 0 || event.getDownTime() > 0) { + serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, true); + } else { + serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, false); + } ContextCompat.startForegroundService(context, serviceIntent); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java index 7585e9d33..74735a264 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java @@ -132,7 +132,7 @@ public class PlayerWidgetJobService extends SafeJobIntentService { views.setImageViewBitmap(R.id.imgvCover, icon); } catch (Throwable tr) { Log.e(TAG, "Error loading the media icon for the widget", tr); - views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_foreground); + views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round); } views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle()); @@ -171,7 +171,7 @@ public class PlayerWidgetJobService extends SafeJobIntentService { views.setViewVisibility(R.id.txtvProgress, View.GONE); views.setViewVisibility(R.id.txtvTitle, View.GONE); views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE); - views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_foreground); + views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round); views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/ProviderInstallerInterceptor.java b/core/src/main/java/de/danoeh/antennapod/core/service/ProviderInstallerInterceptor.java deleted file mode 100644 index 4fa1fc3d7..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/ProviderInstallerInterceptor.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.danoeh.antennapod.core.service; - -import androidx.annotation.NonNull; -import okhttp3.Interceptor; -import okhttp3.Response; - -import java.io.IOException; - -public class ProviderInstallerInterceptor implements Interceptor { - public static Runnable installer = () -> { }; - - @Override - @NonNull - public Response intercept(Chain chain) throws IOException { - installer.run(); - return chain.proceed(chain.request()); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java index 9d0b3c5ad..a01b3cb52 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java @@ -1,40 +1,16 @@ package de.danoeh.antennapod.core.service.download; import android.os.Build; -import androidx.annotation.NonNull; import android.text.TextUtils; import android.util.Log; - -import de.danoeh.antennapod.core.service.BasicAuthorizationInterceptor; - -import java.io.File; -import java.io.IOException; -import java.net.CookieManager; -import java.net.CookiePolicy; -import java.net.HttpURLConnection; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.Socket; -import java.net.SocketAddress; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - +import androidx.annotation.NonNull; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.ProviderInstallerInterceptor; +import de.danoeh.antennapod.core.service.BasicAuthorizationInterceptor; import de.danoeh.antennapod.core.service.UserAgentInterceptor; +import de.danoeh.antennapod.core.ssl.BackportTrustManager; +import de.danoeh.antennapod.core.ssl.NoV1SslSocketFactory; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.Flavors; import okhttp3.Cache; import okhttp3.CipherSuite; import okhttp3.ConnectionSpec; @@ -46,6 +22,19 @@ import okhttp3.Request; import okhttp3.Response; import okhttp3.internal.http.StatusLine; +import javax.net.ssl.X509TrustManager; +import java.io.File; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + /** * Provides access to a HttpClient singleton. */ @@ -117,7 +106,6 @@ public class AntennapodHttpClient { } return response; }); - builder.interceptors().add(new ProviderInstallerInterceptor()); builder.interceptors().add(new BasicAuthorizationInterceptor()); builder.networkInterceptors().add(new UserAgentInterceptor()); @@ -151,13 +139,20 @@ public class AntennapodHttpClient { }); } } - if (Build.VERSION.SDK_INT < 21) { - builder.sslSocketFactory(new CustomSslSocketFactory(), trustManager()); + + if (Flavors.FLAVOR == Flavors.FREE) { + // The Free flavor bundles a modern conscrypt (security provider), so CustomSslSocketFactory + // is only used to make sure that modern protocols (TLSv1.3 and TLSv1.2) are enabled and + // that old, deprecated, protocols (like SSLv3, TLSv1.0 and TLSv1.1) are disabled. + X509TrustManager trustManager = BackportTrustManager.create(); + builder.sslSocketFactory(new NoV1SslSocketFactory(trustManager), trustManager); + } else if (Build.VERSION.SDK_INT < 21) { + X509TrustManager trustManager = BackportTrustManager.create(); + builder.sslSocketFactory(new NoV1SslSocketFactory(trustManager), trustManager); // workaround for Android 4.x for certain web sites. // see: https://github.com/square/okhttp/issues/4053#issuecomment-402579554 - List<CipherSuite> cipherSuites = new ArrayList<>( - ConnectionSpec.MODERN_TLS.cipherSuites()); + List<CipherSuite> cipherSuites = new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites()); cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA); cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA); @@ -170,101 +165,7 @@ public class AntennapodHttpClient { return builder; } - /** - * Closes expired connections. This method should be called by the using class once has finished its work with - * the HTTP client. - */ - public static synchronized void cleanup() { - if (httpClient != null) { - // does nothing at the moment - } - } - - private static X509TrustManager trustManager() { - try { - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); - if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { - throw new IllegalStateException("Unexpected default trust managers:" - + Arrays.toString(trustManagers)); - } - return (X509TrustManager) trustManagers[0]; - } catch (Exception e) { - Log.e(TAG, Log.getStackTraceString(e)); - return null; - } - } - public static void setCacheDirectory(File cacheDirectory) { AntennapodHttpClient.cacheDirectory = cacheDirectory; } - - private static class CustomSslSocketFactory extends SSLSocketFactory { - - private SSLSocketFactory factory; - - public CustomSslSocketFactory() { - try { - SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - sslContext.init(null, null, null); - factory= sslContext.getSocketFactory(); - } catch(GeneralSecurityException e) { - e.printStackTrace(); - } - } - - @Override - public String[] getDefaultCipherSuites() { - return factory.getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return factory.getSupportedCipherSuites(); - } - - public Socket createSocket() throws IOException { - SSLSocket result = (SSLSocket) factory.createSocket(); - configureSocket(result); - return result; - } - - public Socket createSocket(String var1, int var2) throws IOException { - SSLSocket result = (SSLSocket) factory.createSocket(var1, var2); - configureSocket(result); - return result; - } - - public Socket createSocket(Socket var1, String var2, int var3, boolean var4) throws IOException { - SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4); - configureSocket(result); - return result; - } - - public Socket createSocket(InetAddress var1, int var2) throws IOException { - SSLSocket result = (SSLSocket) factory.createSocket(var1, var2); - configureSocket(result); - return result; - } - - public Socket createSocket(String var1, int var2, InetAddress var3, int var4) throws IOException { - SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4); - configureSocket(result); - return result; - } - - public Socket createSocket(InetAddress var1, int var2, InetAddress var3, int var4) throws IOException { - SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4); - configureSocket(result); - return result; - } - - private void configureSocket(SSLSocket s) { - s.setEnabledProtocols(new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" } ); - } - - } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index e44aa716a..f1b35fe23 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -10,6 +10,7 @@ import android.content.IntentFilter; import android.os.Binder; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.text.TextUtils; import android.util.Log; @@ -178,7 +179,7 @@ public class DownloadService extends Service { public void onCreate() { Log.d(TAG, "Service started"); isRunning = true; - handler = new Handler(); + handler = new Handler(Looper.getMainLooper()); notificationManager = new DownloadServiceNotification(this); IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java index 64666d25d..975bc3cb3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java @@ -153,7 +153,11 @@ public class DownloadServiceNotification { iconId = R.drawable.ic_notification_sync_error; intent = ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(context); id = R.id.notification_download_report; - content = String.format(context.getString(R.string.download_report_content), successfulDownloads, failedDownloads); + content = context.getResources() + .getQuantityString(R.plurals.download_report_content, + successfulDownloads, + successfulDownloads, + failedDownloads); } NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); 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 61608992b..65b7ed7d1 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 @@ -71,6 +71,7 @@ public class HttpDownloader extends Downloader { // set header explicitly so that okhttp doesn't do transparent gzip Log.d(TAG, "addHeader(\"Accept-Encoding\", \"identity\")"); httpReq.addHeader("Accept-Encoding", "identity"); + httpReq.cacheControl(new CacheControl.Builder().noCache().build()); // noStore breaks CDNs } if (!TextUtils.isEmpty(request.getLastModified())) { @@ -259,7 +260,6 @@ public class HttpDownloader extends Downloader { onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); } finally { IOUtils.closeQuietly(out); - AntennapodHttpClient.cleanup(); IOUtils.closeQuietly(responseBody); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java index c50162788..18c5fce27 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java @@ -18,7 +18,6 @@ import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; import java.io.File; import java.io.IOException; -import java.util.Date; import java.util.concurrent.Callable; public class FeedParserTask implements Callable<FeedHandlerResult> { @@ -104,13 +103,6 @@ public class FeedParserTask implements Callable<FeedHandlerResult> { if (item.getTitle() == null) { throw new InvalidFeedException("Item has no title: " + item); } - if (item.getPubDate() == null) { - Log.e(TAG, "Item has no pubDate. Using current time as pubDate"); - if (item.getTitle() != null) { - Log.e(TAG, "Title of invalid item: " + item.getTitle()); - } - item.setPubDate(new Date()); - } } } 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 bf485e55f..9f53c6da0 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 @@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.service.playback; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.app.UiModeManager; import android.bluetooth.BluetoothA2dp; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -10,6 +11,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.media.AudioManager; import android.media.MediaPlayer; @@ -83,6 +85,7 @@ import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -314,7 +317,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { } } emitter.onSuccess(queueItems); - }).subscribe(queueItems -> mediaSession.setQueue(queueItems), Throwable::printStackTrace); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(queueItems -> mediaSession.setQueue(queueItems), Throwable::printStackTrace); flavorHelper.initializeMediaPlayer(PlaybackService.this); mediaSession.setActive(true); @@ -451,6 +457,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { notificationManager.cancel(R.id.notification_streaming_confirmation); final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); + final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false); final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false); Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); if (keycode == -1 && playable == null && !castDisconnect) { @@ -464,8 +471,15 @@ public class PlaybackService extends MediaBrowserServiceCompat { stateManager.stopForeground(true); } else { if (keycode != -1) { - Log.d(TAG, "Received media button event"); - boolean handled = handleKeycode(keycode, true); + boolean notificationButton; + if (hardwareButton) { + Log.d(TAG, "Received hardware button event"); + notificationButton = false; + } else { + Log.d(TAG, "Received media button event"); + notificationButton = true; + } + boolean handled = handleKeycode(keycode, notificationButton); if (!handled && !stateManager.hasReceivedValidStartCommand()) { stateManager.stopService(); return Service.START_NOT_STICKY; @@ -1179,6 +1193,20 @@ public class PlaybackService extends MediaBrowserServiceCompat { capabilities = capabilities | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; } + UiModeManager uiModeManager = (UiModeManager) getApplicationContext().getSystemService(Context.UI_MODE_SERVICE); + if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { + sessionState.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( + CUSTOM_ACTION_REWIND, + getString(R.string.rewind_label), R.drawable.ic_notification_fast_rewind) + .build()); + sessionState.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( + CUSTOM_ACTION_FAST_FORWARD, + getString(R.string.fast_forward_label), R.drawable.ic_notification_fast_forward) + .build()); + } + sessionState.setActions(capabilities); flavorHelper.sessionStateAddActionForWear(sessionState, diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java index 3239f3378..632ac07ea 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java @@ -85,7 +85,7 @@ public class PlaybackServiceNotificationBuilder { private Bitmap getDefaultIcon() { if (defaultIcon == null) { - defaultIcon = getBitmap(context, R.drawable.notification_default_large_icon); + defaultIcon = getBitmap(context, R.mipmap.ic_launcher); } return defaultIcon; } @@ -136,9 +136,10 @@ public class PlaybackServiceNotificationBuilder { notification.setContentIntent(getPlayerActivityPendingIntent()); notification.setWhen(0); - notification.setSmallIcon(R.drawable.ic_antenna); + notification.setSmallIcon(R.drawable.ic_notification); notification.setOngoing(false); notification.setOnlyAlertOnce(true); + notification.setShowWhen(false); notification.setPriority(UserPreferences.getNotifyPriority()); notification.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); notification.setColor(NotificationCompat.COLOR_DEFAULT); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java index 55212cd46..05d64ea3e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import android.util.Log; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; +import io.reactivex.disposables.Disposable; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -57,7 +58,7 @@ public class PlaybackServiceTaskManager { private ScheduledFuture<?> widgetUpdaterFuture; private ScheduledFuture<?> sleepTimerFuture; private volatile Future<List<FeedItem>> queueFuture; - private volatile Future<?> chapterLoaderFuture; + private volatile Disposable chapterLoaderFuture; private SleepTimer sleepTimer; @@ -102,7 +103,7 @@ public class PlaybackServiceTaskManager { private synchronized void loadQueue() { if (!isQueueLoaderActive()) { - queueFuture = schedExecutor.submit(DBReader::getQueue); + queueFuture = schedExecutor.submit(() -> DBReader.getQueue()); } } @@ -289,28 +290,19 @@ public class PlaybackServiceTaskManager { } } - private synchronized void cancelChapterLoader() { - if (isChapterLoaderActive()) { - chapterLoaderFuture.cancel(true); - } - } - - private synchronized boolean isChapterLoaderActive() { - return chapterLoaderFuture != null && !chapterLoaderFuture.isDone(); - } - /** * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active, * it will be cancelled first. * On completion, the callback's onChapterLoaded method will be called. */ public synchronized void startChapterLoader(@NonNull final Playable media) { - if (isChapterLoaderActive()) { - cancelChapterLoader(); + if (chapterLoaderFuture != null) { + chapterLoaderFuture.dispose(); + chapterLoaderFuture = null; } if (media.getChapters() == null) { - Completable.create(emitter -> { + chapterLoaderFuture = Completable.create(emitter -> { media.loadChapterMarks(context); emitter.onComplete(); }) @@ -330,7 +322,11 @@ public class PlaybackServiceTaskManager { cancelWidgetUpdater(); disableSleepTimer(); cancelQueueLoader(); - cancelChapterLoader(); + + if (chapterLoaderFuture != null) { + chapterLoaderFuture.dispose(); + chapterLoaderFuture = null; + } } /** @@ -347,7 +343,7 @@ public class PlaybackServiceTaskManager { if (Looper.myLooper() == Looper.getMainLooper()) { // Called in main thread => ExoPlayer is used // Run on ui thread even if called from schedExecutor - Handler handler = new Handler(); + Handler handler = new Handler(Looper.getMainLooper()); return () -> handler.post(runnable); } else { return runnable; @@ -374,7 +370,7 @@ public class PlaybackServiceTaskManager { if (UserPreferences.useExoplayer() && Looper.myLooper() == Looper.getMainLooper()) { // Run callbacks in main thread so they can call ExoPlayer methods themselves - this.handler = new Handler(); + this.handler = new Handler(Looper.getMainLooper()); } else { this.handler = null; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java b/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java new file mode 100644 index 000000000..720d6a9d9 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java @@ -0,0 +1,73 @@ +package de.danoeh.antennapod.core.ssl; + +public class BackportCaCerts { + public static final String SECTIGO_USER_TRUST = "-----BEGIN CERTIFICATE-----\n" + + "MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB\n" + + "iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\n" + + "cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\n" + + "BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw\n" + + "MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV\n" + + "BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU\n" + + "aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy\n" + + "dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\n" + + "AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B\n" + + "3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY\n" + + "tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/\n" + + "Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2\n" + + "VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT\n" + + "79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6\n" + + "c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT\n" + + "Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l\n" + + "c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee\n" + + "UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE\n" + + "Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd\n" + + "BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G\n" + + "A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF\n" + + "Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO\n" + + "VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3\n" + + "ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs\n" + + "8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR\n" + + "iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze\n" + + "Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ\n" + + "XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/\n" + + "qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB\n" + + "VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB\n" + + "L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG\n" + + "jjxDah2nGN59PRbxYvnKkKj9\n" + + "-----END CERTIFICATE-----\n"; + + public static final String COMODO = "-----BEGIN CERTIFICATE-----\n" + + "MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB\n" + + "hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\n" + + "A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV\n" + + "BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5\n" + + "MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT\n" + + "EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR\n" + + "Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\n" + + "dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR\n" + + "6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X\n" + + "pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC\n" + + "9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV\n" + + "/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf\n" + + "Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z\n" + + "+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w\n" + + "qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah\n" + + "SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC\n" + + "u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf\n" + + "Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq\n" + + "crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E\n" + + "FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB\n" + + "/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl\n" + + "wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM\n" + + "4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV\n" + + "2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna\n" + + "FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ\n" + + "CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK\n" + + "boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke\n" + + "jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL\n" + + "S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb\n" + + "QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl\n" + + "0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB\n" + + "NVOFBkpdn627G190\n" + + "-----END CERTIFICATE-----"; +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java b/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java new file mode 100644 index 000000000..b8fe950b2 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java @@ -0,0 +1,58 @@ +package de.danoeh.antennapod.core.ssl; + +import android.util.Log; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateFactory; +import java.util.ArrayList; +import java.util.List; + +/** + * SSL trust manager that allows old Android systems to use modern certificates. + */ +public class BackportTrustManager { + private static final String TAG = "BackportTrustManager"; + + private static X509TrustManager getSystemTrustManager(KeyStore keystore) { + TrustManagerFactory factory; + try { + factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init(keystore); + for (TrustManager manager : factory.getTrustManagers()) { + if (manager instanceof X509TrustManager) { + return (X509TrustManager) manager; + } + } + } catch (NoSuchAlgorithmException | KeyStoreException e) { + e.printStackTrace(); + } + throw new IllegalStateException("Unexpected default trust managers"); + } + + public static X509TrustManager create() { + try { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(null); // Clear + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + keystore.setCertificateEntry("BACKPORT_COMODO_ROOT_CA", cf.generateCertificate( + new ByteArrayInputStream(BackportCaCerts.COMODO.getBytes(Charset.forName("UTF-8"))))); + keystore.setCertificateEntry("SECTIGO_USER_TRUST_CA", cf.generateCertificate( + new ByteArrayInputStream(BackportCaCerts.SECTIGO_USER_TRUST.getBytes(Charset.forName("UTF-8"))))); + + List<X509TrustManager> managers = new ArrayList<>(); + managers.add(getSystemTrustManager(keystore)); + managers.add(getSystemTrustManager(null)); + return new CompositeX509TrustManager(managers); + } catch (Exception e) { + Log.e(TAG, Log.getStackTraceString(e)); + return null; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java b/core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java new file mode 100644 index 000000000..7af96a492 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java @@ -0,0 +1,60 @@ +package de.danoeh.antennapod.core.ssl; + +import javax.net.ssl.X509TrustManager; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents an ordered list of {@link X509TrustManager}s with additive trust. If any one of the composed managers + * trusts a certificate chain, then it is trusted by the composite manager. + * Based on https://stackoverflow.com/a/16229909 + */ +public class CompositeX509TrustManager implements X509TrustManager { + private final List<X509TrustManager> trustManagers; + + public CompositeX509TrustManager(List<X509TrustManager> trustManagers) { + this.trustManagers = trustManagers; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + CertificateException reason = null; + for (X509TrustManager trustManager : trustManagers) { + try { + trustManager.checkClientTrusted(chain, authType); + return; // someone trusts them. success! + } catch (CertificateException e) { + // maybe someone else will trust them + reason = e; + } + } + throw reason; + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + CertificateException reason = null; + for (X509TrustManager trustManager : trustManagers) { + try { + trustManager.checkServerTrusted(chain, authType); + return; // someone trusts them. success! + } catch (CertificateException e) { + // maybe someone else will trust them + reason = e; + } + } + throw reason; + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + List<X509Certificate> certificates = new ArrayList<>(); + for (X509TrustManager trustManager : trustManagers) { + certificates.addAll(Arrays.asList(trustManager.getAcceptedIssuers())); + } + return certificates.toArray(new X509Certificate[0]); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java b/core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java new file mode 100644 index 000000000..96a42f22d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java @@ -0,0 +1,100 @@ +package de.danoeh.antennapod.core.ssl; + +import de.danoeh.antennapod.core.util.Flavors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.security.GeneralSecurityException; + +/** + * SSLSocketFactory that does not use TLS 1.0 + * This fixes issues with old Android versions that abort if the server does not know TLS 1.0 + */ +public class NoV1SslSocketFactory extends SSLSocketFactory { + private SSLSocketFactory factory; + + public NoV1SslSocketFactory(TrustManager trustManager) { + try { + SSLContext sslContext; + + if (Flavors.FLAVOR == Flavors.FREE) { + // Free flavor (bundles modern conscrypt): support for TLSv1.3 is guaranteed. + sslContext = SSLContext.getInstance("TLSv1.3"); + } else { + // Play flavor (security provider can vary): only TLSv1.2 is guaranteed. + sslContext = SSLContext.getInstance("TLSv1.2"); + } + + sslContext.init(null, new TrustManager[] {trustManager}, null); + factory = sslContext.getSocketFactory(); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } + } + + @Override + public String[] getDefaultCipherSuites() { + return factory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return factory.getSupportedCipherSuites(); + } + + public Socket createSocket() throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(); + configureSocket(result); + return result; + } + + public Socket createSocket(String var1, int var2) throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(var1, var2); + configureSocket(result); + return result; + } + + public Socket createSocket(Socket var1, String var2, int var3, boolean var4) throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4); + configureSocket(result); + return result; + } + + public Socket createSocket(InetAddress var1, int var2) throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(var1, var2); + configureSocket(result); + return result; + } + + public Socket createSocket(String var1, int var2, InetAddress var3, int var4) throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4); + configureSocket(result); + return result; + } + + public Socket createSocket(InetAddress var1, int var2, InetAddress var3, int var4) throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4); + configureSocket(result); + return result; + } + + private void configureSocket(SSLSocket s) { + if (Flavors.FLAVOR == Flavors.FREE) { + // Free flavor (bundles modern conscrypt): TLSv1.3 and modern cipher suites are + // guaranteed. Protocols older than TLSv1.2 are now deprecated and can be disabled. + s.setEnabledProtocols(new String[] { "TLSv1.3", "TLSv1.2" }); + } else { + // Play flavor (security provider can vary): only TLSv1.2 is guaranteed, supported + // cipher suites may vary. Old protocols might be necessary to keep things working. + + // TLS 1.0 is enabled by default on some old systems, which causes connection errors. + // This disables that. + s.setEnabledProtocols(new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" }); + } + } +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index 0de67b306..892254507 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -19,6 +19,7 @@ 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.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.util.LongIntMap; @@ -143,6 +144,7 @@ public final class DBReader { Feed feed = feedIndex.get(item.getFeedId()); if (feed == null) { Log.w(TAG, "No match found for item with ID " + item.getId() + ". Feed ID was " + item.getFeedId()); + feed = new Feed("", "", "Error: Item without feed"); } item.setFeed(feed); } @@ -744,6 +746,7 @@ public final class DBReader { long episodesStarted = 0; long episodesStartedIncludingMarked = 0; long totalDownloadSize = 0; + long episodesDownloadCount = 0; List<FeedItem> items = getFeed(feed.getId()).getItems(); for (FeedItem item : items) { FeedMedia media = item.getMedia(); @@ -771,13 +774,14 @@ public final class DBReader { if (media.isDownloaded()) { totalDownloadSize = totalDownloadSize + media.getSize(); + episodesDownloadCount++; } episodes++; } feedTime.add(new StatisticsItem( feed, feedTotalTime, feedPlayedTime, feedPlayedTimeCountAll, episodes, - episodesStarted, episodesStartedIncludingMarked, totalDownloadSize)); + episodesStarted, episodesStartedIncludingMarked, totalDownloadSize, episodesDownloadCount)); } adapter.close(); @@ -794,6 +798,7 @@ public final class DBReader { Log.d(TAG, "getNavDrawerData() called with: " + ""); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); + List<Feed> feeds = getFeedList(adapter); long[] feedIds = new long[feeds.size()]; for (int i = 0; i < feeds.size(); i++) { @@ -801,15 +806,8 @@ public final class DBReader { } final LongIntMap feedCounters = adapter.getFeedCounters(feedIds); - int feedFilter = UserPreferences.getFeedFilter(); - if (feedFilter == UserPreferences.FEED_FILTER_COUNTER_ZERO) { - for (int i = feeds.size() - 1; i >= 0; i--) { - if (feedCounters.get(feeds.get(i).getId()) <= 0) { - feedCounters.delete(feeds.get(i).getId()); - feeds.remove(i); - } - } - } + SubscriptionsFilter subscriptionsFilter = UserPreferences.getSubscriptionsFilter(); + feeds = subscriptionsFilter.filter(getFeedList(adapter), feedCounters); Comparator<Feed> comparator; int feedOrder = UserPreferences.getFeedOrder(); @@ -854,24 +852,11 @@ public final class DBReader { } }; } else { + final Map<Long, Long> recentPubDates = adapter.getMostRecentItemDates(); comparator = (lhs, rhs) -> { - if (lhs.getItems() == null || lhs.getItems().size() == 0) { - List<FeedItem> items = DBReader.getFeedItemList(lhs); - lhs.setItems(items); - } - if (rhs.getItems() == null || rhs.getItems().size() == 0) { - List<FeedItem> items = DBReader.getFeedItemList(rhs); - rhs.setItems(items); - } - if (lhs.getMostRecentItem() == null) { - return 1; - } else if (rhs.getMostRecentItem() == null) { - return -1; - } else { - Date d1 = lhs.getMostRecentItem().getPubDate(); - Date d2 = rhs.getMostRecentItem().getPubDate(); - return d2.compareTo(d1); - } + long dateLhs = recentPubDates.containsKey(lhs.getId()) ? recentPubDates.get(lhs.getId()) : 0; + long dateRhs = recentPubDates.containsKey(rhs.getId()) ? recentPubDates.get(rhs.getId()) : 0; + return Long.compare(dateRhs, dateLhs); }; } 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 477a39968..c059e696a 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 @@ -263,7 +263,6 @@ public final class DBTasks { EventBus.getDefault().post(new MessageEvent(context.getString(R.string.error_file_not_found))); } - @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public static List<? extends FeedItem> enqueueFeedItemsToDownload(final Context context, List<? extends FeedItem> items) throws InterruptedException, ExecutionException { List<FeedItem> itemsToEnqueue = new ArrayList<>(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java index 234c01b20..271babc6e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java @@ -5,6 +5,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.ParcelFileDescriptor; +import android.text.format.Formatter; import android.util.Log; import de.danoeh.antennapod.core.R; import org.apache.commons.io.FileUtils; @@ -53,7 +54,16 @@ public class DatabaseExporter { if (currentDB.exists()) { src = new FileInputStream(currentDB).getChannel(); dst = outFileStream.getChannel(); - dst.transferFrom(src, 0, src.size()); + long srcSize = src.size(); + dst.transferFrom(src, 0, srcSize); + + long newDstSize = dst.size(); + if (newDstSize != srcSize) { + throw new IOException(String.format( + "Unable to write entire database. Expected to write %s, but wrote %s.", + Formatter.formatShortFileSize(context, srcSize), + Formatter.formatShortFileSize(context, newDstSize))); + } } else { throw new IOException("Can not access current database"); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java index 4c594783a..935b06cd6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -21,9 +21,11 @@ import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; -import java.util.Collections; +import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import de.danoeh.antennapod.core.feed.Chapter; @@ -609,6 +611,11 @@ public class PodDBAdapter { * @return the id of the entry */ private long setFeedItem(FeedItem item, boolean saveFeed) { + if (item.getId() == 0 && item.getPubDate() == null) { + Log.e(TAG, "Newly saved item has no pubDate. Using current date as pubDate"); + item.setPubDate(new Date()); + } + ContentValues values = new ContentValues(); values.put(KEY_TITLE, item.getTitle()); values.put(KEY_LINK, item.getLink()); @@ -1217,6 +1224,25 @@ public class PodDBAdapter { return conditionalFeedCounterRead(whereRead, feedIds); } + public final Map<Long, Long> getMostRecentItemDates() { + final String query = "SELECT " + KEY_FEED + "," + + " MAX(" + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + ") AS most_recent_pubdate" + + " FROM " + TABLE_NAME_FEED_ITEMS + + " GROUP BY " + KEY_FEED; + + Cursor c = db.rawQuery(query, null); + Map<Long, Long> result = new HashMap<>(); + if (c.moveToFirst()) { + do { + long feedId = c.getLong(0); + long date = c.getLong(1); + result.put(feedId, date); + } while (c.moveToNext()); + } + c.close(); + return result; + } + public final int getNumberOfDownloadedEpisodes() { final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_DOWNLOADED + " > 0"; @@ -1234,12 +1260,17 @@ public class PodDBAdapter { * Uses DatabaseUtils to escape a search query and removes ' at the * beginning and the end of the string returned by the escape method. */ - private String prepareSearchQuery(String query) { - StringBuilder builder = new StringBuilder(); - DatabaseUtils.appendEscapedSQLString(builder, query); - builder.deleteCharAt(0); - builder.deleteCharAt(builder.length() - 1); - return builder.toString(); + private String[] prepareSearchQuery(String query) { + String[] queryWords = query.split("\\s+"); + for (int i = 0; i < queryWords.length; ++i) { + StringBuilder builder = new StringBuilder(); + DatabaseUtils.appendEscapedSQLString(builder, queryWords[i]); + builder.deleteCharAt(0); + builder.deleteCharAt(builder.length() - 1); + queryWords[i] = builder.toString(); + } + + return queryWords; } /** @@ -1249,7 +1280,7 @@ public class PodDBAdapter { * @return A cursor with all search results in SEL_FI_EXTRA selection. */ public Cursor searchItems(long feedID, String searchQuery) { - String preparedQuery = prepareSearchQuery(searchQuery); + String[] queryWords = prepareSearchQuery(searchQuery); String queryFeedId; if (feedID != 0) { @@ -1260,14 +1291,28 @@ public class PodDBAdapter { queryFeedId = "1 = 1"; } - String query = SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION - + " WHERE " + queryFeedId + " AND (" - + KEY_DESCRIPTION + " LIKE '%" + preparedQuery + "%' OR " - + KEY_CONTENT_ENCODED + " LIKE '%" + preparedQuery + "%' OR " - + KEY_TITLE + " LIKE '%" + preparedQuery + "%'" - + ") ORDER BY " + KEY_PUBDATE + " DESC " - + "LIMIT 300"; - return db.rawQuery(query, null); + String queryStart = SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION + + " WHERE " + queryFeedId + " AND ("; + StringBuilder sb = new StringBuilder(queryStart); + + for (int i = 0; i < queryWords.length; i++) { + sb + .append("(") + .append(KEY_DESCRIPTION + " LIKE '%").append(queryWords[i]) + .append("%' OR ") + .append(KEY_CONTENT_ENCODED).append(" LIKE '%").append(queryWords[i]) + .append("%' OR ") + .append(KEY_TITLE).append(" LIKE '%").append(queryWords[i]) + .append("%') "); + + if (i != queryWords.length - 1) { + sb.append("AND "); + } + } + + sb.append(") ORDER BY " + KEY_PUBDATE + " DESC LIMIT 300"); + + return db.rawQuery(sb.toString(), null); } /** @@ -1276,15 +1321,31 @@ public class PodDBAdapter { * @return A cursor with all search results in SEL_FI_EXTRA selection. */ public Cursor searchFeeds(String searchQuery) { - String preparedQuery = prepareSearchQuery(searchQuery); - String query = "SELECT * FROM " + TABLE_NAME_FEEDS + " WHERE " - + KEY_TITLE + " LIKE '%" + preparedQuery + "%' OR " - + KEY_CUSTOM_TITLE + " LIKE '%" + preparedQuery + "%' OR " - + KEY_AUTHOR + " LIKE '%" + preparedQuery + "%' OR " - + KEY_DESCRIPTION + " LIKE '%" + preparedQuery + "%' " - + "ORDER BY " + KEY_TITLE + " ASC " - + "LIMIT 300"; - return db.rawQuery(query, null); + String[] queryWords = prepareSearchQuery(searchQuery); + + String queryStart = "SELECT * FROM " + TABLE_NAME_FEEDS + " WHERE "; + StringBuilder sb = new StringBuilder(queryStart); + + for (int i = 0; i < queryWords.length; i++) { + sb + .append("(") + .append(KEY_TITLE).append(" LIKE '%").append(queryWords[i]) + .append("%' OR ") + .append(KEY_CUSTOM_TITLE).append(" LIKE '%").append(queryWords[i]) + .append("%' OR ") + .append(KEY_AUTHOR).append(" LIKE '%").append(queryWords[i]) + .append("%' OR ") + .append(KEY_DESCRIPTION).append(" LIKE '%").append(queryWords[i]) + .append("%') "); + + if (i != queryWords.length - 1) { + sb.append("AND "); + } + } + + sb.append("ORDER BY " + KEY_TITLE + " ASC LIMIT 300"); + + return db.rawQuery(sb.toString(), null); } /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java b/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java index f96af185b..18a5403a7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java @@ -36,9 +36,14 @@ public class StatisticsItem { */ public final long totalDownloadSize; + /** + * Stores the number of episodes downloaded. + */ + public final long episodesDownloadCount; + public StatisticsItem(Feed feed, long time, long timePlayed, long timePlayedCountAll, long episodes, long episodesStarted, long episodesStartedIncludingMarked, - long totalDownloadSize) { + long totalDownloadSize, long episodesDownloadCount) { this.feed = feed; this.time = time; this.timePlayed = timePlayed; @@ -47,5 +52,6 @@ public class StatisticsItem { this.episodesStarted = episodesStarted; this.episodesStartedIncludingMarked = episodesStartedIncludingMarked; this.totalDownloadSize = totalDownloadSize; + this.episodesDownloadCount = episodesDownloadCount; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java index 4c89ebc19..1f5d9b75f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java @@ -39,6 +39,7 @@ import de.danoeh.antennapod.core.sync.model.ISyncService; import de.danoeh.antennapod.core.sync.model.SubscriptionChanges; import de.danoeh.antennapod.core.sync.model.SyncServiceException; import de.danoeh.antennapod.core.sync.model.UploadChangesResponse; +import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.URLChecker; import de.danoeh.antennapod.core.util.gui.NotificationUtils; import io.reactivex.Completable; @@ -456,7 +457,7 @@ public class SyncService extends Worker { break; } } - + LongList queueToBeRemoved = new LongList(); List<FeedItem> updatedItems = new ArrayList<>(); for (EpisodeAction action : mostRecentPlayAction.values()) { FeedItem playItem = DBReader.getFeedItemByUrl(action.getPodcast(), action.getEpisode()); @@ -467,10 +468,12 @@ public class SyncService extends Worker { if (playItem.getMedia().hasAlmostEnded()) { Log.d(TAG, "Marking as played"); playItem.setPlayed(true); + queueToBeRemoved.add(playItem.getId()); } updatedItems.add(playItem); } } + DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray()); DBWriter.setItemList(updatedItems); } @@ -491,7 +494,7 @@ public class SyncService extends Worker { PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT); Notification notification = new NotificationCompat.Builder(getApplicationContext(), - NotificationUtils.CHANNEL_ID_ERROR) + NotificationUtils.CHANNEL_ID_SYNC_ERROR) .setContentTitle(getApplicationContext().getString(R.string.gpodnetsync_error_title)) .setContentText(description) .setContentIntent(pendingIntent) diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java index a9e46e42c..8cca2f28f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java @@ -2,9 +2,12 @@ package de.danoeh.antennapod.core.util; import android.content.Context; import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; +import android.os.Build; import androidx.core.net.ConnectivityManagerCompat; import android.text.TextUtils; import android.util.Log; @@ -29,65 +32,65 @@ import okhttp3.Response; public class NetworkUtils { private NetworkUtils(){} - private static final String TAG = NetworkUtils.class.getSimpleName(); - - private static Context context; - - public static void init(Context context) { - NetworkUtils.context = context; - } - - /** - * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for - * automatic downloads is disabled or the device is connected to a Wi-Fi - * network that is on the 'selected networks' list of the Wi-Fi filter for - * automatic downloads and false otherwise. - * */ - public static boolean autodownloadNetworkAvailable() { - ConnectivityManager cm = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = cm.getActiveNetworkInfo(); - if (networkInfo != null) { - if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { - Log.d(TAG, "Device is connected to Wi-Fi"); - if (networkInfo.isConnected()) { - if (!UserPreferences.isEnableAutodownloadWifiFilter()) { - Log.d(TAG, "Auto-dl filter is disabled"); - return true; - } else { - WifiManager wm = (WifiManager) context.getApplicationContext() - .getSystemService(Context.WIFI_SERVICE); - WifiInfo wifiInfo = wm.getConnectionInfo(); - List<String> selectedNetworks = Arrays - .asList(UserPreferences - .getAutodownloadSelectedNetworks()); - if (selectedNetworks.contains(Integer.toString(wifiInfo - .getNetworkId()))) { - Log.d(TAG, "Current network is on the selected networks list"); - return true; - } - } - } - } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) { - Log.d(TAG, "Device is connected to Ethernet"); - if (networkInfo.isConnected()) { - return true; - } - } else { - if (!UserPreferences.isAllowMobileAutoDownload()) { - Log.d(TAG, "Auto Download not enabled on Mobile"); - return false; - } - if (networkInfo.isRoaming()) { - Log.d(TAG, "Roaming on foreign network"); - return false; - } - return true; - } - } - Log.d(TAG, "Network for auto-dl is not available"); - return false; - } + private static final String TAG = NetworkUtils.class.getSimpleName(); + + private static Context context; + + public static void init(Context context) { + NetworkUtils.context = context; + } + + /** + * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for + * automatic downloads is disabled or the device is connected to a Wi-Fi + * network that is on the 'selected networks' list of the Wi-Fi filter for + * automatic downloads and false otherwise. + * */ + public static boolean autodownloadNetworkAvailable() { + ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + if (networkInfo != null) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + Log.d(TAG, "Device is connected to Wi-Fi"); + if (networkInfo.isConnected()) { + if (!UserPreferences.isEnableAutodownloadWifiFilter()) { + Log.d(TAG, "Auto-dl filter is disabled"); + return true; + } else { + WifiManager wm = (WifiManager) context.getApplicationContext() + .getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wm.getConnectionInfo(); + List<String> selectedNetworks = Arrays + .asList(UserPreferences + .getAutodownloadSelectedNetworks()); + if (selectedNetworks.contains(Integer.toString(wifiInfo + .getNetworkId()))) { + Log.d(TAG, "Current network is on the selected networks list"); + return true; + } + } + } + } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) { + Log.d(TAG, "Device is connected to Ethernet"); + if (networkInfo.isConnected()) { + return true; + } + } else { + if (!UserPreferences.isAllowMobileAutoDownload()) { + Log.d(TAG, "Auto Download not enabled on Mobile"); + return false; + } + if (networkInfo.isRoaming()) { + Log.d(TAG, "Roaming on foreign network"); + return false; + } + return true; + } + } + Log.d(TAG, "Network for auto-dl is not available"); + return false; + } public static boolean networkAvailable() { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -96,7 +99,7 @@ public class NetworkUtils { } public static boolean isEpisodeDownloadAllowed() { - return UserPreferences.isAllowMobileEpisodeDownload() || !NetworkUtils.isNetworkMetered(); + return UserPreferences.isAllowMobileEpisodeDownload() || !NetworkUtils.isNetworkRestricted(); } public static boolean isEpisodeHeadDownloadAllowed() { @@ -106,22 +109,53 @@ public class NetworkUtils { } public static boolean isImageAllowed() { - return UserPreferences.isAllowMobileImages() || !NetworkUtils.isNetworkMetered(); + return UserPreferences.isAllowMobileImages() || !NetworkUtils.isNetworkRestricted(); } public static boolean isStreamingAllowed() { - return UserPreferences.isAllowMobileStreaming() || !NetworkUtils.isNetworkMetered(); + return UserPreferences.isAllowMobileStreaming() || !NetworkUtils.isNetworkRestricted(); } public static boolean isFeedRefreshAllowed() { - return UserPreferences.isAllowMobileFeedRefresh() || !NetworkUtils.isNetworkMetered(); + return UserPreferences.isAllowMobileFeedRefresh() || !NetworkUtils.isNetworkRestricted(); + } + + public static boolean isNetworkRestricted() { + return isNetworkMetered() || isNetworkCellular(); } - private static boolean isNetworkMetered() { - ConnectivityManager connManager = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); + private static boolean isNetworkMetered() { + ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); return ConnectivityManagerCompat.isActiveNetworkMetered(connManager); - } + } + + private static boolean isNetworkCellular() { + ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (Build.VERSION.SDK_INT >= 23) { + Network network = connManager.getActiveNetwork(); + if (network == null) { + return false; // Nothing connected + } + NetworkInfo info = connManager.getNetworkInfo(network); + if (info == null) { + return true; // Better be safe than sorry + } + NetworkCapabilities capabilities = connManager.getNetworkCapabilities(network); + if (capabilities == null) { + return true; // Better be safe than sorry + } + return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); + } else { + // if the default network is a VPN, + // this method will return the NetworkInfo for one of its underlying networks + NetworkInfo info = connManager.getActiveNetworkInfo(); + if (info == null) { + return false; // Nothing connected + } + //noinspection deprecation + return info.getType() == ConnectivityManager.TYPE_MOBILE; + } + } /** * Returns the SSID of the wifi connection, or <code>null</code> if there is no wifi. @@ -135,7 +169,7 @@ public class NetworkUtils { return null; } - public static Single<Long> getFeedMediaSizeObservable(FeedMedia media) { + public static Single<Long> getFeedMediaSizeObservable(FeedMedia media) { return Single.create((SingleOnSubscribe<Long>) emitter -> { if (!NetworkUtils.isEpisodeHeadDownloadAllowed()) { emitter.onSuccess(0L); @@ -188,7 +222,7 @@ public class NetworkUtils { DBWriter.setFeedMedia(media); }) .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + .observeOn(AndroidSchedulers.mainThread()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java index 51fe2da78..ad81a1d17 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java @@ -4,16 +4,20 @@ import java.util.Comparator; import de.danoeh.antennapod.core.feed.FeedItem; -/** Compares the pubDate of two FeedItems for sorting*/ +/** + * Compares the pubDate of two FeedItems for sorting. + */ public class FeedItemPubdateComparator implements Comparator<FeedItem> { - /** Returns a new instance of this comparator in reverse order. - public static FeedItemPubdateComparator newInstance() { - FeedItemPubdateComparator - }*/ - @Override - public int compare(FeedItem lhs, FeedItem rhs) { - return rhs.getPubDate().compareTo(lhs.getPubDate()); - } + /** + * Returns a new instance of this comparator in reverse order. + */ + @Override + public int compare(FeedItem lhs, FeedItem rhs) { + if (rhs.getPubDate() == null || lhs.getPubDate() == null) { + return 0; + } + return rhs.getPubDate().compareTo(lhs.getPubDate()); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/exception/RxJavaErrorHandlerSetup.java b/core/src/main/java/de/danoeh/antennapod/core/util/exception/RxJavaErrorHandlerSetup.java deleted file mode 100644 index 223104d2e..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/exception/RxJavaErrorHandlerSetup.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.danoeh.antennapod.core.util.exception; - -import android.util.Log; -import io.reactivex.exceptions.UndeliverableException; -import io.reactivex.plugins.RxJavaPlugins; - -public class RxJavaErrorHandlerSetup { - - private RxJavaErrorHandlerSetup() { - - } - - public static void setupRxJavaErrorHandler() { - RxJavaPlugins.setErrorHandler(e -> { - if (e instanceof UndeliverableException) { - // Probably just disposed because the fragment was left - Log.d("RxJavaErrorHandler", "Ignored exception: " + Log.getStackTraceString(e)); - return; - } - Thread.currentThread().getUncaughtExceptionHandler() - .uncaughtException(Thread.currentThread(), e); - }); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java index f546ca019..ddbe68938 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java @@ -14,6 +14,7 @@ public class NotificationUtils { public static final String CHANNEL_ID_DOWNLOADING = "downloading"; public static final String CHANNEL_ID_PLAYING = "playing"; public static final String CHANNEL_ID_ERROR = "error"; + public static final String CHANNEL_ID_SYNC_ERROR = "sync_error"; public static final String CHANNEL_ID_AUTO_DOWNLOAD = "auto_download"; public static void createChannels(Context context) { @@ -27,6 +28,7 @@ public class NotificationUtils { mNotificationManager.createNotificationChannel(createChannelDownloading(context)); mNotificationManager.createNotificationChannel(createChannelPlaying(context)); mNotificationManager.createNotificationChannel(createChannelError(context)); + mNotificationManager.createNotificationChannel(createChannelSyncError(context)); mNotificationManager.createNotificationChannel(createChannelAutoDownload(context)); } } @@ -66,6 +68,14 @@ public class NotificationUtils { } @RequiresApi(api = Build.VERSION_CODES.O) + private static NotificationChannel createChannelSyncError(Context c) { + NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID_SYNC_ERROR, + c.getString(R.string.notification_channel_sync_error), NotificationManager.IMPORTANCE_HIGH); + notificationChannel.setDescription(c.getString(R.string.notification_channel_sync_error_description)); + return notificationChannel; + } + + @RequiresApi(api = Build.VERSION_CODES.O) private static NotificationChannel createChannelAutoDownload(Context c) { NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_AUTO_DOWNLOAD, c.getString(R.string.notification_channel_auto_download), NotificationManager.IMPORTANCE_DEFAULT); |