summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java45
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java48
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java34
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java129
-rw-r--r--app/src/main/res/layout/audioplayer_fragment.xml2
-rw-r--r--build.gradle1
-rw-r--r--core/build.gradle6
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java55
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java35
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java35
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java40
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java30
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/Optional.java213
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java287
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java17
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java6
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java18
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java3
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java7
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/util/id3reader/ChapterReaderTest.java23
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/util/playback/ExternalMediaTest.java56
-rw-r--r--core/src/test/resources/media-parser/hindenburg-journalist-pro.m4abin0 -> 23315 bytes
-rw-r--r--core/src/test/resources/media-parser/hindenburg-journalist-pro.mp3bin0 -> 206098 bytes
-rw-r--r--net/README.md3
-rw-r--r--net/ssl/README.md3
-rw-r--r--net/ssl/build.gradle65
-rw-r--r--net/ssl/src/free/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java13
-rw-r--r--net/ssl/src/main/AndroidManifest.xml1
-rw-r--r--net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/BackportCaCerts.java (renamed from core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java)2
-rw-r--r--net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/BackportTrustManager.java (renamed from core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java)2
-rw-r--r--net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/CompositeX509TrustManager.java (renamed from core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java)2
-rw-r--r--net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/NoV1SslSocketFactory.java (renamed from core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java)8
-rw-r--r--net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/SslClientSetup.java37
-rw-r--r--net/ssl/src/play/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java20
-rw-r--r--settings.gradle1
-rw-r--r--ui/app-start-intent/build.gradle1
-rw-r--r--ui/common/build.gradle1
45 files changed, 478 insertions, 831 deletions
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
index c6f2cc84b..efd1d53ca 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
@@ -19,6 +19,7 @@ import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@@ -54,7 +55,6 @@ import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeExceptio
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.FileNameGenerator;
import de.danoeh.antennapod.core.util.IntentUtils;
-import de.danoeh.antennapod.core.util.Optional;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.URLChecker;
import de.danoeh.antennapod.core.util.playback.RemoteMedia;
@@ -63,9 +63,11 @@ import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText;
import de.danoeh.antennapod.databinding.OnlinefeedviewActivityBinding;
import de.danoeh.antennapod.dialog.AuthenticationDialog;
import de.danoeh.antennapod.discovery.PodcastSearcherRegistry;
+import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
+import io.reactivex.observers.DisposableMaybeObserver;
import io.reactivex.schedulers.Schedulers;
import org.apache.commons.lang3.StringUtils;
import org.greenrobot.eventbus.EventBus;
@@ -320,33 +322,46 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
}
Log.d(TAG, "Parsing feed");
- parser = Observable.fromCallable(this::doParseFeed)
+ parser = Maybe.fromCallable(this::doParseFeed)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
- .subscribe(
- optionalResult -> {
- if (optionalResult.isPresent()) {
- FeedHandlerResult result = optionalResult.get();
- beforeShowFeedInformation(result.feed);
- showFeedInformation(result.feed, result.alternateFeedUrls);
- }
- }, error -> {
+ .subscribeWith(new DisposableMaybeObserver<FeedHandlerResult>() {
+ @Override
+ public void onSuccess(@NonNull FeedHandlerResult result) {
+ beforeShowFeedInformation(result.feed);
+ showFeedInformation(result.feed, result.alternateFeedUrls);
+ }
+
+ @Override
+ public void onComplete() {
+ // Ignore null result: We showed the discovery dialog.
+ }
+
+ @Override
+ public void onError(@NonNull Throwable error) {
showErrorDialog(error.getMessage(), "");
Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error));
- });
+ }
+ });
}
- @NonNull
- private Optional<FeedHandlerResult> doParseFeed() throws Exception {
+ /**
+ * Try to parse the feed.
+ * @return The FeedHandlerResult if successful.
+ * Null if unsuccessful but we started another attempt.
+ * @throws Exception If unsuccessful but we do not know a resolution.
+ */
+ @Nullable
+ private FeedHandlerResult doParseFeed() throws Exception {
FeedHandler handler = new FeedHandler();
try {
- return Optional.of(handler.parseFeed(feed));
+ return handler.parseFeed(feed);
} catch (UnsupportedFeedtypeException e) {
Log.d(TAG, "Unsupported feed type detected");
if ("html".equalsIgnoreCase(e.getRootElement())) {
boolean dialogShown = showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url());
if (dialogShown) {
- return Optional.empty();
+ return null; // Should not display an error message
} else {
throw new UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html));
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java
index bdaae1bea..51f264e56 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java
@@ -29,12 +29,14 @@ import de.danoeh.antennapod.activity.CastEnabledActivity;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.event.FavoritesEvent;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
+import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.TimeSpeedConverter;
@@ -46,6 +48,7 @@ import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
import de.danoeh.antennapod.dialog.SleepTimerDialog;
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
+import de.danoeh.antennapod.view.ChapterSeekBar;
import de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView;
import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -63,7 +66,7 @@ import java.util.List;
* Shows the audio player.
*/
public class AudioPlayerFragment extends Fragment implements
- SeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener {
+ ChapterSeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener {
public static final String TAG = "AudioPlayerFragment";
private static final int POS_COVER = 0;
private static final int POS_DESCR = 1;
@@ -77,7 +80,7 @@ public class AudioPlayerFragment extends Fragment implements
private ViewPager2 pager;
private TextView txtvPosition;
private TextView txtvLength;
- private SeekBar sbPosition;
+ private ChapterSeekBar sbPosition;
private ImageButton butRev;
private TextView txtvRev;
private ImageButton butPlay;
@@ -172,12 +175,33 @@ public class AudioPlayerFragment extends Fragment implements
return root;
}
- public void setHasChapters(boolean hasChapters) {
+ private void setHasChapters(boolean hasChapters) {
this.hasChapters = hasChapters;
tabLayoutMediator.detach();
tabLayoutMediator.attach();
}
+ private void setChapterDividers(Playable media) {
+
+ if (media == null) {
+ return;
+ }
+
+ float[] dividerPos = null;
+
+ if (hasChapters) {
+ List<Chapter> chapters = media.getChapters();
+ dividerPos = new float[chapters.size()];
+ float duration = media.getDuration();
+
+ for (int i = 0; i < chapters.size(); i++) {
+ dividerPos[i] = chapters.get(i).getStart() / duration;
+ }
+ }
+
+ sbPosition.setDividerPos(dividerPos);
+ }
+
public View getExternalPlayerHolder() {
return getView().findViewById(R.id.playerFragment);
}
@@ -298,16 +322,17 @@ public class AudioPlayerFragment extends Fragment implements
disposable = Maybe.create(emitter -> {
Playable media = controller.getMedia();
if (media != null) {
+ ChapterUtils.loadChapters(media, getContext());
emitter.onSuccess(media);
} else {
emitter.onComplete();
}
})
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(media -> updateUi((Playable) media),
- error -> Log.e(TAG, Log.getStackTraceString(error)),
- () -> updateUi(null));
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(media -> updateUi((Playable) media),
+ error -> Log.e(TAG, Log.getStackTraceString(error)),
+ () -> updateUi(null));
}
private PlaybackController newPlaybackController() {
@@ -389,8 +414,15 @@ public class AudioPlayerFragment extends Fragment implements
if (controller == null) {
return;
}
+
+ if (media != null && media.getChapters() != null) {
+ setHasChapters(media.getChapters().size() > 0);
+ } else {
+ setHasChapters(false);
+ }
updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration()));
updatePlaybackSpeedButton(media);
+ setChapterDividers(media);
setupOptionsMenu(media);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
index b578a603f..acda462bd 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
@@ -118,7 +118,7 @@ public class ChaptersFragment extends Fragment {
disposable = Maybe.create(emitter -> {
Playable media = controller.getMedia();
if (media != null) {
- media.loadChapterMarks(getContext());
+ ChapterUtils.loadChapters(media, getContext());
emitter.onSuccess(media);
} else {
emitter.onComplete();
@@ -137,7 +137,6 @@ public class ChaptersFragment extends Fragment {
return;
}
adapter.setMedia(media);
- ((AudioPlayerFragment) getParentFragment()).setHasChapters(adapter.getItemCount() > 0);
int positionOfCurrentChapter = getCurrentChapter(media);
updateChapterSelection(positionOfCurrentChapter);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
index c86fdc070..acb929dd2 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
@@ -56,7 +56,6 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.FeedItemPermutors;
import de.danoeh.antennapod.core.util.FeedItemUtil;
-import de.danoeh.antennapod.core.util.Optional;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil;
import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment;
@@ -549,27 +548,32 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
disposable = Observable.fromCallable(this::loadData)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
- .subscribe(result -> {
- feed = result.orElse(null);
- refreshHeaderView();
- displayList();
- }, error -> Log.e(TAG, Log.getStackTraceString(error)));
+ .subscribe(
+ result -> {
+ feed = result;
+ refreshHeaderView();
+ displayList();
+ }, error -> {
+ feed = null;
+ refreshHeaderView();
+ displayList();
+ Log.e(TAG, Log.getStackTraceString(error));
+ });
}
- @NonNull
- private Optional<Feed> loadData() {
- Feed feed = DBReader.getFeed(feedID);
- if (feed != null && feed.getItemFilter() != null) {
- DBReader.loadAdditionalFeedItemListData(feed.getItems());
- FeedItemFilter filter = feed.getItemFilter();
- feed.setItems(filter.filter(feed.getItems()));
+ @Nullable
+ private Feed loadData() {
+ Feed feed = DBReader.getFeed(feedID, true);
+ if (feed == null) {
+ return null;
}
- if (feed != null && feed.getSortOrder() != null) {
+ DBReader.loadAdditionalFeedItemListData(feed.getItems());
+ if (feed.getSortOrder() != null) {
List<FeedItem> feedItems = feed.getItems();
FeedItemPermutors.getPermutor(feed.getSortOrder()).reorder(feedItems);
feed.setItems(feedItems);
}
- return Optional.ofNullable(feed);
+ return feed;
}
private static class FeedItemListAdapter extends EpisodeItemListAdapter {
diff --git a/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java b/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java
new file mode 100644
index 000000000..5e80198d5
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java
@@ -0,0 +1,129 @@
+package de.danoeh.antennapod.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import de.danoeh.antennapod.ui.common.ThemeUtils;
+
+public class ChapterSeekBar extends androidx.appcompat.widget.AppCompatSeekBar {
+
+ private float top;
+ private float width;
+ private float bottom;
+ private float density;
+ private float progressPrimary;
+ private float progressSecondary;
+ private float[] dividerPos;
+ private final Paint paintBackground = new Paint();
+ private final Paint paintProgressPrimary = new Paint();
+ private final Paint paintProgressSecondary = new Paint();
+
+ public ChapterSeekBar(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public ChapterSeekBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public ChapterSeekBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ private void init(Context context) {
+ setBackground(null); // Removes the thumb shadow
+ dividerPos = null;
+ density = context.getResources().getDisplayMetrics().density;
+ paintBackground.setColor(ThemeUtils.getColorFromAttr(getContext(),
+ de.danoeh.antennapod.core.R.attr.currently_playing_background));
+ paintBackground.setAlpha(128);
+ paintProgressPrimary.setColor(ThemeUtils.getColorFromAttr(getContext(),
+ de.danoeh.antennapod.core.R.attr.colorPrimary));
+ paintProgressSecondary.setColor(ThemeUtils.getColorFromAttr(getContext(),
+ de.danoeh.antennapod.core.R.attr.seek_background));
+ }
+
+ /**
+ * Sets the relative positions of the chapter dividers.
+ * @param dividerPos of the chapter dividers relative to the duration of the media.
+ */
+ public void setDividerPos(final float[] dividerPos) {
+ if (dividerPos != null) {
+ this.dividerPos = new float[dividerPos.length + 2];
+ this.dividerPos[0] = 0;
+ System.arraycopy(dividerPos, 0, this.dividerPos, 1, dividerPos.length);
+ this.dividerPos[this.dividerPos.length - 1] = 1;
+ } else {
+ this.dividerPos = null;
+ }
+ }
+
+ @Override
+ protected synchronized void onDraw(Canvas canvas) {
+ top = getTop() + density * 7.5f;
+ bottom = getBottom() - density * 7.5f;
+ width = (float) (getRight() - getPaddingRight() - getLeft() - getPaddingLeft());
+ progressSecondary = getSecondaryProgress() / (float) getMax() * width;
+ progressPrimary = getProgress() / (float) getMax() * width;
+
+ if (dividerPos == null) {
+ drawProgress(canvas);
+ } else {
+ drawProgressChapters(canvas);
+ }
+ drawThumb(canvas);
+ }
+
+ private void drawProgress(Canvas canvas) {
+ final int saveCount = canvas.save();
+ canvas.translate(getPaddingLeft(), getPaddingTop());
+ canvas.drawRect(0, top, width, bottom, paintBackground);
+ canvas.drawRect(0, top, progressSecondary, bottom, paintProgressSecondary);
+ canvas.drawRect(0, top, progressPrimary, bottom, paintProgressPrimary);
+ canvas.restoreToCount(saveCount);
+ }
+
+ private void drawProgressChapters(Canvas canvas) {
+ final int saveCount = canvas.save();
+ int currChapter = 1;
+ float chapterMargin = density * 0.6f;
+ float topExpanded = getTop() + density * 7;
+ float bottomExpanded = getBottom() - density * 7;
+
+ canvas.translate(getPaddingLeft(), getPaddingTop());
+
+ for (int i = 1; i < dividerPos.length; i++) {
+ float right = dividerPos[i] * width - chapterMargin;
+ float left = dividerPos[i - 1] * width + chapterMargin;
+ float rightCurr = dividerPos[currChapter] * width - chapterMargin;
+ float leftCurr = dividerPos[currChapter - 1] * width + chapterMargin;
+
+ canvas.drawRect(left, top, right, bottom, paintBackground);
+
+ if (right < progressPrimary) {
+ currChapter = i + 1;
+ canvas.drawRect(left, top, right, bottom, paintProgressPrimary);
+ } else if (isPressed()) {
+ canvas.drawRect(leftCurr, topExpanded, rightCurr, bottomExpanded, paintBackground);
+ canvas.drawRect(leftCurr, topExpanded, progressPrimary, bottomExpanded, paintProgressPrimary);
+ } else {
+ if (progressSecondary > leftCurr) {
+ canvas.drawRect(leftCurr, top, progressSecondary, bottom, paintProgressSecondary);
+ }
+ canvas.drawRect(leftCurr, top, progressPrimary, bottom, paintProgressPrimary);
+ }
+ }
+ canvas.restoreToCount(saveCount);
+ }
+
+ private void drawThumb(Canvas canvas) {
+ final int saveCount = canvas.save();
+ canvas.translate(getPaddingLeft() - getThumbOffset(), getPaddingTop());
+ getThumb().draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+}
diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml
index 62e0a5e3a..f77e96338 100644
--- a/app/src/main/res/layout/audioplayer_fragment.xml
+++ b/app/src/main/res/layout/audioplayer_fragment.xml
@@ -88,7 +88,7 @@
android:layoutDirection="ltr"
android:orientation="vertical">
- <SeekBar
+ <de.danoeh.antennapod.view.ChapterSeekBar
android:id="@+id/sbPosition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/build.gradle b/build.gradle
index aa7f8ebbe..e4c95ae30 100644
--- a/build.gradle
+++ b/build.gradle
@@ -73,6 +73,7 @@ project.ext {
// Google Play build
wearableSupportVersion = "2.6.0"
+ playServicesVersion = "8.4.0"
//Tests
awaitilityVersion = "3.1.6"
diff --git a/core/build.gradle b/core/build.gradle
index e68fa9f97..75ad7faad 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -71,6 +71,7 @@ android {
}
dependencies {
+ implementation project(':net:ssl')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
@@ -102,13 +103,10 @@ dependencies {
// Non-free dependencies:
playApi 'com.google.android.libraries.cast.companionlibrary:ccl:2.9.1'
playApi 'androidx.mediarouter:mediarouter:1.0.0'
- playApi 'com.google.android.gms:play-services-cast:8.4.0'
+ playApi "com.google.android.gms:play-services-cast:$playServicesVersion"
playApi "com.google.android.support:wearable:$wearableSupportVersion"
compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion"
- // bundle conscrypt with free builds
- freeImplementation "org.conscrypt:conscrypt-android:$conscryptVersion"
-
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
testImplementation 'junit:junit:4.13'
testImplementation 'org.mockito:mockito-inline:3.5.13'
diff --git a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
index 0193bf8ce..755bec14e 100644
--- a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -1,8 +1,8 @@
package de.danoeh.antennapod.core;
import android.content.Context;
-import java.security.Security;
-import org.conscrypt.Conscrypt;
+
+import de.danoeh.antennapod.net.ssl.SslProviderInstaller;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
@@ -42,16 +42,11 @@ public class ClientConfig {
UserPreferences.init(context);
UsageStatistics.init(context);
PlaybackPreferences.init(context);
- installSslProvider(context);
+ SslProviderInstaller.install(context);
NetworkUtils.init(context);
AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp"));
SleepTimerPreferences.init(context);
NotificationUtils.createChannels(context);
initialized = true;
}
-
- private static void installSslProvider(Context context) {
- // Insert bundled conscrypt as highest security provider (overrides OS version).
- Security.insertProviderAt(Conscrypt.newProvider(), 1);
- }
}
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 bd30a3953..ac742e765 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
@@ -1,15 +1,7 @@
package de.danoeh.antennapod.core.feed;
import android.text.TextUtils;
-
-import java.util.ArrayList;
import java.util.Arrays;
-import java.util.List;
-
-import de.danoeh.antennapod.core.storage.DBReader;
-import de.danoeh.antennapod.core.util.LongList;
-
-import static de.danoeh.antennapod.core.feed.FeedItem.TAG_FAVORITE;
public class FeedItemFilter {
@@ -58,53 +50,6 @@ public class FeedItemFilter {
return Arrays.asList(properties).contains(property);
}
- /**
- * Run a list of feed items through the filter.
- */
- public List<FeedItem> filter(List<FeedItem> items) {
- if (properties.length == 0) {
- return items;
- }
-
- List<FeedItem> result = new ArrayList<>();
-
- // Check for filter combinations that will always return an empty list
- // (e.g. requiring played and unplayed at the same time)
- if (showPlayed && showUnplayed) return result;
- if (showQueued && showNotQueued) return result;
- if (showDownloaded && showNotDownloaded) return result;
-
- final LongList queuedIds = DBReader.getQueueIDList();
- for (FeedItem item : items) {
- // If the item does not meet a requirement, skip it.
-
- if (showPlayed && !item.isPlayed()) continue;
- if (showUnplayed && item.isPlayed()) continue;
-
- if (showPaused && item.getState() != FeedItem.State.IN_PROGRESS) continue;
- if (showNotPaused && item.getState() == FeedItem.State.IN_PROGRESS) continue;
-
- boolean queued = queuedIds.contains(item.getId());
- if (showQueued && !queued) continue;
- if (showNotQueued && queued) continue;
-
- boolean downloaded = item.getMedia() != null && item.getMedia().isDownloaded();
- if (showDownloaded && !downloaded) continue;
- if (showNotDownloaded && downloaded) continue;
-
- if (showHasMedia && !item.hasMedia()) continue;
- if (showNoMedia && item.hasMedia()) continue;
-
- if (showIsFavorite && !item.isTagged(TAG_FAVORITE)) continue;
- if (showNotFavorite && item.isTagged(TAG_FAVORITE)) continue;
-
- // If the item reaches here, it meets all criteria
- result.add(item);
- }
-
- return result;
- }
-
public String[] getValues() {
return properties.clone();
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
index 9049a3ba9..34c9b8ca7 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
@@ -12,7 +12,6 @@ import androidx.annotation.Nullable;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat;
-import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
@@ -24,7 +23,6 @@ import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
-import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.model.EpisodeAction;
@@ -386,33 +384,6 @@ public class FeedMedia extends FeedFile implements Playable {
}
@Override
- public void loadChapterMarks(Context context) {
- if (item == null && itemID != 0) {
- item = DBReader.getFeedItem(itemID);
- }
- if (item == null || item.getChapters() != null) {
- return;
- }
-
- List<Chapter> chapters = loadChapters(context);
- if (chapters == null) {
- // Do not try loading again. There are no chapters.
- item.setChapters(Collections.emptyList());
- } else {
- item.setChapters(chapters);
- }
- }
-
- private List<Chapter> loadChapters(Context context) {
- List<Chapter> chaptersFromDatabase = null;
- if (item.hasChapters()) {
- chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(item);
- }
- List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(this, context);
- return ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
- }
-
- @Override
public String getEpisodeTitle() {
if (item == null) {
return null;
@@ -493,6 +464,10 @@ public class FeedMedia extends FeedFile implements Playable {
return download_url != null;
}
+ public long getItemId() {
+ return itemID;
+ }
+
@Override
public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timeStamp) {
if(item != null && item.isNew()) {
@@ -549,7 +524,7 @@ public class FeedMedia extends FeedFile implements Playable {
@Override
public void setChapters(List<Chapter> chapters) {
- if(item != null) {
+ if (item != null) {
item.setChapters(chapters);
}
}
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 a01b3cb52..c4029d57f 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,19 +1,14 @@
package de.danoeh.antennapod.core.service.download;
-import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.core.preferences.UserPreferences;
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 de.danoeh.antennapod.net.ssl.SslClientSetup;
import okhttp3.Cache;
-import okhttp3.CipherSuite;
-import okhttp3.ConnectionSpec;
import okhttp3.Credentials;
import okhttp3.HttpUrl;
import okhttp3.JavaNetCookieJar;
@@ -21,8 +16,6 @@ import okhttp3.OkHttpClient;
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;
@@ -30,9 +23,6 @@ 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;
/**
@@ -140,28 +130,7 @@ public class AntennapodHttpClient {
}
}
- 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());
- cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
- cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
-
- ConnectionSpec legacyTls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
- .cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
- .build();
- builder.connectionSpecs(Arrays.asList(legacyTls, ConnectionSpec.CLEARTEXT));
- }
-
+ SslClientSetup.installCertificates(builder);
return builder;
}
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 650827e97..9430e2e3c 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
@@ -76,7 +76,6 @@ import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
-import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlayableException;
import de.danoeh.antennapod.core.util.playback.PlayableUtils;
@@ -519,8 +518,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false);
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
- //If the user asks to play External Media, the casting session, if on, should end.
- flavorHelper.castDisconnect(playable instanceof ExternalMedia);
if (allowStreamAlways) {
UserPreferences.setAllowMobileStreaming(true);
}
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 b9bc0c712..556d9b3c0 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 de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.widget.WidgetUpdater;
import io.reactivex.disposables.Disposable;
import org.greenrobot.eventbus.EventBus;
@@ -315,7 +316,7 @@ public class PlaybackServiceTaskManager {
if (media.getChapters() == null) {
chapterLoaderFuture = Completable.create(emitter -> {
- media.loadChapterMarks(context);
+ ChapterUtils.loadChapters(media, context);
emitter.onComplete();
})
.subscribeOn(Schedulers.io())
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 fcf61b070..7aa5f8abe 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
@@ -161,11 +161,15 @@ public final class DBReader {
* The method does NOT change the items-attribute of the feed.
*/
public static List<FeedItem> getFeedItemList(final Feed feed) {
+ return getFeedItemList(feed, FeedItemFilter.unfiltered());
+ }
+
+ public static List<FeedItem> getFeedItemList(final Feed feed, final FeedItemFilter filter) {
Log.d(TAG, "getFeedItemList() called with: " + "feed = [" + feed + "]");
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- try (Cursor cursor = adapter.getAllItemsOfFeedCursor(feed)) {
+ try (Cursor cursor = adapter.getItemsOfFeedCursor(feed, filter)) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
Collections.sort(items, new FeedItemPubdateComparator());
for (FeedItem item : items) {
@@ -480,31 +484,41 @@ public final class DBReader {
*
* @param feedId The ID of the Feed
* @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the
- * database and the items-attribute will be set correctly.
+ * database and the items-attribute will be set correctly.
*/
+ @Nullable
public static Feed getFeed(final long feedId) {
- Log.d(TAG, "getFeed() called with: " + "feedId = [" + feedId + "]");
-
- PodDBAdapter adapter = PodDBAdapter.getInstance();
- adapter.open();
- try {
- return getFeed(feedId, adapter);
- } finally {
- adapter.close();
- }
+ return getFeed(feedId, false);
}
+ /**
+ * Loads a specific Feed from the database.
+ *
+ * @param feedId The ID of the Feed
+ * @param filtered <code>true</code> if only the visible items should be loaded according to the feed filter.
+ * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the
+ * database and the items-attribute will be set correctly.
+ */
@Nullable
- static Feed getFeed(final long feedId, PodDBAdapter adapter) {
+ public static Feed getFeed(final long feedId, boolean filtered) {
+ Log.d(TAG, "getFeed() called with: " + "feedId = [" + feedId + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
Feed feed = null;
try (Cursor cursor = adapter.getFeedCursor(feedId)) {
if (cursor.moveToNext()) {
feed = extractFeedFromCursorRow(cursor);
- feed.setItems(getFeedItemList(feed));
+ if (filtered) {
+ feed.setItems(getFeedItemList(feed, feed.getItemFilter()));
+ } else {
+ feed.setItems(getFeedItemList(feed));
+ }
} else {
Log.e(TAG, "getFeed could not find feed with id " + feedId);
}
return feed;
+ } finally {
+ adapter.close();
}
}
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 596ab624e..d16432cd6 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
@@ -320,7 +320,7 @@ public final class DBTasks {
private static Feed searchFeedByIdentifyingValueOrID(PodDBAdapter adapter,
Feed feed) {
if (feed.getId() != 0) {
- return DBReader.getFeed(feed.getId(), adapter);
+ return DBReader.getFeed(feed.getId());
} else {
List<Feed> feeds = DBReader.getFeedList();
for (Feed f : feeds) {
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 adb5e6a74..445b1945b 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
@@ -951,9 +951,12 @@ public class PodDBAdapter {
* @param feed The feed you want to get the FeedItems from.
* @return The cursor of the query
*/
- public final Cursor getAllItemsOfFeedCursor(final Feed feed) {
+ public final Cursor getItemsOfFeedCursor(final Feed feed, FeedItemFilter filter) {
+ String filterQuery = FeedItemFilterQuery.generateFrom(filter);
+ String whereClauseAnd = "".equals(filterQuery) ? "" : " AND " + filterQuery;
final String query = SELECT_FEED_ITEMS_AND_MEDIA
- + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + feed.getId();
+ + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + feed.getId()
+ + whereClauseAnd;
return db.rawQuery(query, null);
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java
index 4ad35d0c2..ca9689048 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java
@@ -6,7 +6,10 @@ import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.ChapterMerger;
+import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
+import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator;
import de.danoeh.antennapod.core.util.id3reader.ChapterReader;
import de.danoeh.antennapod.core.util.id3reader.ID3ReaderException;
@@ -47,6 +50,33 @@ public class ChapterUtils {
return chapters.size() - 1;
}
+ public static void loadChapters(Playable playable, Context context) {
+ if (playable.getChapters() != null) {
+ // Already loaded
+ return;
+ }
+
+ List<Chapter> chaptersFromDatabase = null;
+ if (playable instanceof FeedMedia) {
+ FeedMedia feedMedia = (FeedMedia) playable;
+ if (feedMedia.getItem() == null) {
+ feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId()));
+ }
+ if (feedMedia.getItem().hasChapters()) {
+ chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem());
+ }
+ }
+
+ List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context);
+ List<Chapter> chapters = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
+ if (chapters == null) {
+ // Do not try loading again. There are no chapters.
+ playable.setChapters(Collections.emptyList());
+ } else {
+ playable.setChapters(chapters);
+ }
+ }
+
public static List<Chapter> loadChaptersFromMediaFile(Playable playable, Context context) {
try (CountingInputStream in = openStream(playable, context)) {
List<Chapter> chapters = readId3ChaptersFrom(in);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java b/core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java
deleted file mode 100644
index 5feb232e7..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package de.danoeh.antennapod.core.util;
-
-import de.danoeh.antennapod.core.BuildConfig;
-
-/**
- * Helper class to handle the different build flavors.
- */
-public enum Flavors {
- FREE,
- PLAY,
- UNKNOWN;
-
- public static final Flavors FLAVOR;
-
- static {
- if (BuildConfig.FLAVOR.equals("free")) {
- FLAVOR = FREE;
- } else if (BuildConfig.FLAVOR.equals("play")) {
- FLAVOR = PLAY;
- } else {
- FLAVOR = UNKNOWN;
- }
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java b/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java
deleted file mode 100644
index 37f12c01c..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-package de.danoeh.antennapod.core.util;
-
-import java.util.NoSuchElementException;
-import java.util.Objects;
-
-// AntennaPod's stripped-down version of Java/Android platform's java.util.Optional
-// so that it can be used on lower API level (API level 14)
-
-// Android-changed: removed ValueBased paragraph.
-/**
- * A container object which may or may not contain a non-null value.
- * If a value is present, {@code isPresent()} will return {@code true} and
- * {@code get()} will return the value.
- *
- * <p>Additional methods that depend on the presence or absence of a contained
- * value are provided, such as {@link #orElse(java.lang.Object) orElse()}
- * (return a default value if value not present) and
- * {@link #ifPresent(java.util.function.Consumer) ifPresent()} (execute a block
- * of code if the value is present).
- *
- * @since 1.8
- */
-public final class Optional<T> {
- /**
- * Common instance for {@code empty()}.
- */
- private static final Optional<?> EMPTY = new Optional<>();
-
- /**
- * If non-null, the value; if null, indicates no value is present
- */
- private final T value;
-
- /**
- * Constructs an empty instance.
- *
- * @implNote Generally only one empty instance, {@link Optional#EMPTY},
- * should exist per VM.
- */
- private Optional() {
- this.value = null;
- }
-
- /**
- * Returns an empty {@code Optional} instance. No value is present for this
- * Optional.
- *
- * @apiNote Though it may be tempting to do so, avoid testing if an object
- * is empty by comparing with {@code ==} against instances returned by
- * {@code Option.empty()}. There is no guarantee that it is a singleton.
- * Instead, use {@link #isPresent()}.
- *
- * @param <T> Type of the non-existent value
- * @return an empty {@code Optional}
- */
- public static <T> Optional<T> empty() {
- @SuppressWarnings("unchecked")
- Optional<T> t = (Optional<T>) EMPTY;
- return t;
- }
-
- /**
- * Constructs an instance with the value present.
- *
- * @param value the non-null value to be present
- * @throws NullPointerException if value is null
- */
- private Optional(T value) {
- this.value = Objects.requireNonNull(value);
- }
-
- /**
- * Returns an {@code Optional} with the specified present non-null value.
- *
- * @param <T> the class of the value
- * @param value the value to be present, which must be non-null
- * @return an {@code Optional} with the value present
- * @throws NullPointerException if value is null
- */
- public static <T> Optional<T> of(T value) {
- return new Optional<>(value);
- }
-
- /**
- * Returns an {@code Optional} describing the specified value, if non-null,
- * otherwise returns an empty {@code Optional}.
- *
- * @param <T> the class of the value
- * @param value the possibly-null value to describe
- * @return an {@code Optional} with a present value if the specified value
- * is non-null, otherwise an empty {@code Optional}
- */
- public static <T> Optional<T> ofNullable(T value) {
- return value == null ? empty() : of(value);
- }
-
- /**
- * If a value is present in this {@code Optional}, returns the value,
- * otherwise throws {@code NoSuchElementException}.
- *
- * @return the non-null value held by this {@code Optional}
- * @throws NoSuchElementException if there is no value present
- *
- * @see Optional#isPresent()
- */
- public T get() {
- if (value == null) {
- throw new NoSuchElementException("No value present");
- }
- return value;
- }
-
- /**
- * Return {@code true} if there is a value present, otherwise {@code false}.
- *
- * @return {@code true} if there is a value present, otherwise {@code false}
- */
- public boolean isPresent() {
- return value != null;
- }
-
-
- /**
- * Return the value if present, otherwise return {@code other}.
- *
- * @param other the value to be returned if there is no value present, may
- * be null
- * @return the value, if present, otherwise {@code other}
- */
- public T orElse(T other) {
- return value != null ? value : other;
- }
-
- /**
- * Indicates whether some other object is "equal to" this Optional. The
- * other object is considered equal if:
- * <ul>
- * <li>it is also an {@code Optional} and;
- * <li>both instances have no value present or;
- * <li>the present values are "equal to" each other via {@code equals()}.
- * </ul>
- *
- * @param obj an object to be tested for equality
- * @return {code true} if the other object is "equal to" this object
- * otherwise {@code false}
- */
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
-
- if (!(obj instanceof Optional)) {
- return false;
- }
-
- Optional<?> other = (Optional<?>) obj;
- return (value == other.value) || (value != null && value.equals(other.value));
- }
-
- /**
- * Returns the hash code value of the present value, if any, or 0 (zero) if
- * no value is present.
- *
- * @return hash code value of the present value or 0 if no value is present
- */
- @Override
- public int hashCode() {
- return value != null ? value.hashCode() : 0;
- }
-
- /**
- * Returns a non-empty string representation of this Optional suitable for
- * debugging. The exact presentation format is unspecified and may vary
- * between implementations and versions.
- *
- * @implSpec If a value is present the result must include its string
- * representation in the result. Empty and present Optionals must be
- * unambiguously differentiable.
- *
- * @return the string representation of this instance
- */
- @Override
- public String toString() {
- return value != null
- ? String.format("Optional[%s]", value)
- : "Optional.empty";
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java
deleted file mode 100644
index 007658626..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java
+++ /dev/null
@@ -1,287 +0,0 @@
-package de.danoeh.antennapod.core.util.playback;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.media.MediaMetadataRetriever;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-import de.danoeh.antennapod.core.feed.Chapter;
-import de.danoeh.antennapod.core.feed.MediaType;
-import de.danoeh.antennapod.core.util.ChapterUtils;
-import de.danoeh.antennapod.core.util.DateUtils;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.Callable;
-import org.apache.commons.io.FilenameUtils;
-
-/** Represents a media file that is stored on the local storage device. */
-public class ExternalMedia implements Playable {
- public static final int PLAYABLE_TYPE_EXTERNAL_MEDIA = 2;
- public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl";
- public static final String PREF_POSITION = "ExternalMedia.PrefPosition";
- public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType";
- public static final String PREF_LAST_PLAYED_TIME = "ExternalMedia.PrefLastPlayedTime";
-
- private final String source;
- private String episodeTitle;
- private String feedTitle;
- private MediaType mediaType;
- private Date pubDate;
- private List<Chapter> chapters;
- private int duration;
- private int position;
- private long lastPlayedTime;
-
- /**
- * Creates a new playable for files on the sd card.
- * @param source File path of the file
- * @param mediaType Type of the file
- */
- public ExternalMedia(String source, MediaType mediaType) {
- super();
- this.source = source;
- this.mediaType = mediaType;
- }
-
- /**
- * Creates a new playable for files on the sd card.
- * @param source File path of the file
- * @param mediaType Type of the file
- * @param position Position to start from
- * @param lastPlayedTime Timestamp when it was played last
- */
- public ExternalMedia(String source, MediaType mediaType, int position, long lastPlayedTime) {
- this(source, mediaType);
- this.position = position;
- this.lastPlayedTime = lastPlayedTime;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(source);
- dest.writeString(mediaType.toString());
- dest.writeInt(position);
- dest.writeLong(lastPlayedTime);
- }
-
- @Override
- public void writeToPreferences(Editor prefEditor) {
- prefEditor.putString(PREF_SOURCE_URL, source);
- prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString());
- prefEditor.putInt(PREF_POSITION, position);
- prefEditor.putLong(PREF_LAST_PLAYED_TIME, lastPlayedTime);
- }
-
- @Override
- public void loadMetadata() throws PlayableException {
- MediaMetadataRetriever mmr = new MediaMetadataRetriever();
- try {
- mmr.setDataSource(source);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- throw new PlayableException("IllegalArgumentException when setting up MediaMetadataReceiver");
- } catch (RuntimeException e) {
- // http://code.google.com/p/android/issues/detail?id=39770
- e.printStackTrace();
- throw new PlayableException("RuntimeException when setting up MediaMetadataRetriever");
- }
- episodeTitle = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
- if (episodeTitle == null) {
- episodeTitle = FilenameUtils.getName(source);
- }
- feedTitle = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
- try {
- duration = Integer.parseInt(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
- } catch (NumberFormatException e) {
- e.printStackTrace();
- throw new PlayableException("NumberFormatException when reading duration of media file");
- }
-
- String dateStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
- if (!TextUtils.isEmpty(dateStr)) {
- try {
- SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault());
- pubDate = simpleDateFormat.parse(dateStr);
- } catch (ParseException parseException) {
- pubDate = DateUtils.parse(dateStr);
- }
- } else {
- pubDate = null;
- }
- }
-
- @Override
- public void loadChapterMarks(Context context) {
- setChapters(ChapterUtils.loadChaptersFromMediaFile(this, context));
- }
-
- @Override
- public String getEpisodeTitle() {
- return episodeTitle;
- }
-
- @Override
- public Callable<String> loadShownotes() {
- return () -> "";
- }
-
- @Override
- public List<Chapter> getChapters() {
- return chapters;
- }
-
- @Override
- public String getWebsiteLink() {
- return null;
- }
-
- @Override
- public String getPaymentLink() {
- return null;
- }
-
- @Override
- public String getFeedTitle() {
- return feedTitle;
- }
-
- @Override
- public Object getIdentifier() {
- return source;
- }
-
- @Override
- public int getDuration() {
- return duration;
- }
-
- @Override
- public Date getPubDate() {
- return pubDate;
- }
-
- @Override
- public int getPosition() {
- return position;
- }
-
- @Override
- public long getLastPlayedTime() {
- return lastPlayedTime;
- }
-
- @Override
- public MediaType getMediaType() {
- return mediaType;
- }
-
- @Override
- public String getLocalMediaUrl() {
- return source;
- }
-
- @Override
- public String getStreamUrl() {
- return null;
- }
-
- @Override
- public boolean localFileAvailable() {
- return true;
- }
-
- @Override
- public boolean streamAvailable() {
- return false;
- }
-
- @Override
- public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp) {
- SharedPreferences.Editor editor = pref.edit();
- editor.putInt(PREF_POSITION, newPosition);
- editor.putLong(PREF_LAST_PLAYED_TIME, timestamp);
- position = newPosition;
- lastPlayedTime = timestamp;
- editor.apply();
- }
-
- @Override
- public void setPosition(int newPosition) {
- position = newPosition;
- }
-
- @Override
- public void setDuration(int newDuration) {
- duration = newDuration;
- }
-
- @Override
- public void setLastPlayedTime(long lastPlayedTime) {
- this.lastPlayedTime = lastPlayedTime;
- }
-
- @Override
- public void onPlaybackStart() {
-
- }
-
- @Override
- public void onPlaybackPause(Context context) {
-
- }
-
- @Override
- public void onPlaybackCompleted(Context context) {
-
- }
-
- @Override
- public int getPlayableType() {
- return PLAYABLE_TYPE_EXTERNAL_MEDIA;
- }
-
- @Override
- public void setChapters(List<Chapter> chapters) {
- this.chapters = chapters;
- }
-
- public static final Parcelable.Creator<ExternalMedia> CREATOR = new Parcelable.Creator<ExternalMedia>() {
- public ExternalMedia createFromParcel(Parcel in) {
- String source = in.readString();
- MediaType type = MediaType.valueOf(in.readString());
- int position = 0;
- if (in.dataAvail() > 0) {
- position = in.readInt();
- }
- long lastPlayedTime = 0;
- if (in.dataAvail() > 0) {
- lastPlayedTime = in.readLong();
- }
-
- return new ExternalMedia(source, type, position, lastPlayedTime);
- }
-
- public ExternalMedia[] newArray(int size) {
- return new ExternalMedia[size];
- }
- };
-
- @Override
- public String getImageLocation() {
- if (localFileAvailable()) {
- return getLocalMediaUrl();
- } else {
- return null;
- }
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
index 8a4c561f4..f103b32bf 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
@@ -34,13 +34,6 @@ public interface Playable extends Parcelable, ShownotesProvider {
void loadMetadata() throws PlayableException;
/**
- * This method is called from a separate thread by the PlaybackService.
- * Playable objects should load their chapter marks in this method if no
- * local file was available when loadMetadata() was called.
- */
- void loadChapterMarks(Context context);
-
- /**
* Returns the title of the episode that this playable represents
*/
String getEpisodeTitle();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java
index 413058758..861d42c1b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java
@@ -9,7 +9,6 @@ import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import de.danoeh.antennapod.core.feed.FeedMedia;
-import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
@@ -53,9 +52,6 @@ public abstract class PlayableUtils {
case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA:
result = createFeedMediaInstance(pref);
break;
- case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA:
- result = createExternalMediaInstance(pref);
- break;
default:
result = null;
break;
@@ -74,17 +70,4 @@ public abstract class PlayableUtils {
}
return result;
}
-
- private static Playable createExternalMediaInstance(SharedPreferences pref) {
- Playable result = null;
- String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, null);
- String mediaType = pref.getString(ExternalMedia.PREF_MEDIA_TYPE, null);
- if (source != null && mediaType != null) {
- int position = pref.getInt(ExternalMedia.PREF_POSITION, 0);
- long lastPlayedTime = pref.getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, 0);
- result = new ExternalMedia(source, MediaType.valueOf(mediaType),
- position, lastPlayedTime);
- }
- return result;
- }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java
index 7de1a7812..219edd2e7 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java
@@ -11,7 +11,6 @@ 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.MediaType;
-import de.danoeh.antennapod.core.util.ChapterUtils;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
@@ -129,11 +128,6 @@ public class RemoteMedia implements Playable {
}
@Override
- public void loadChapterMarks(Context context) {
- setChapters(ChapterUtils.loadChaptersFromMediaFile(this, context));
- }
-
- @Override
public String getEpisodeTitle() {
return episodeTitle;
}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
index 0225c508a..48de7c6e1 100644
--- a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -2,10 +2,6 @@ package de.danoeh.antennapod.core;
import android.content.Context;
import android.util.Log;
-import com.google.android.gms.common.GoogleApiAvailability;
-import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
-import com.google.android.gms.common.GooglePlayServicesRepairableException;
-import com.google.android.gms.security.ProviderInstaller;
import de.danoeh.antennapod.core.cast.CastManager;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
@@ -15,6 +11,7 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
+import de.danoeh.antennapod.net.ssl.SslProviderInstaller;
import java.io.File;
@@ -48,7 +45,7 @@ public class ClientConfig {
UserPreferences.init(context);
UsageStatistics.init(context);
PlaybackPreferences.init(context);
- installSslProvider(context);
+ SslProviderInstaller.install(context);
NetworkUtils.init(context);
// Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary
// Google Play Service usage.
@@ -64,15 +61,4 @@ public class ClientConfig {
NotificationUtils.createChannels(context);
initialized = true;
}
-
- private static void installSslProvider(Context context) {
- try {
- ProviderInstaller.installIfNeeded(context);
- } catch (GooglePlayServicesRepairableException e) {
- e.printStackTrace();
- GoogleApiAvailability.getInstance().showErrorNotification(context, e.getConnectionStatusCode());
- } catch (GooglePlayServicesNotAvailableException e) {
- e.printStackTrace();
- }
- }
}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
index ab638b568..ffb70adb4 100644
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
@@ -19,7 +19,6 @@ 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.storage.DBReader;
-import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
/**
@@ -53,7 +52,7 @@ public class CastUtils {
public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999;
public static boolean isCastable(Playable media) {
- if (media == null || media instanceof ExternalMedia) {
+ if (media == null) {
return false;
}
if (media instanceof FeedMedia || media instanceof RemoteMedia) {
diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java
index 6c5a9daf1..00fa1b8f5 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java
@@ -1,5 +1,6 @@
package de.danoeh.antennapod.core.storage;
+import de.danoeh.antennapod.core.util.playback.RemoteMedia;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -17,9 +18,7 @@ import de.danoeh.antennapod.core.feed.FeedComponent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.FeedMother;
-import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation;
-import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
import static de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation.AFTER_CURRENTLY_PLAYING;
@@ -105,7 +104,7 @@ public class ItemEnqueuePositionCalculatorTest {
{"case option after currently playing, no currentlyPlaying is null",
concat(TFI_ID, QUEUE_DEFAULT_IDS),
AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, ID_CURRENTLY_PLAYING_NULL},
- {"case option after currently playing, currentlyPlaying is externalMedia",
+ {"case option after currently playing, currentlyPlaying is not a feedMedia",
concat(TFI_ID, QUEUE_DEFAULT_IDS),
AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, ID_CURRENTLY_PLAYING_NOT_FEEDMEDIA},
{"case empty queue, option after currently playing",
@@ -270,7 +269,7 @@ public class ItemEnqueuePositionCalculatorTest {
}
static Playable externalMedia() {
- return new ExternalMedia("http://example.com/episode.mp3", MediaType.AUDIO);
+ return new RemoteMedia(createFeedItem(0));
}
static final long ID_CURRENTLY_PLAYING_NULL = -1L;
diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/id3reader/ChapterReaderTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/id3reader/ChapterReaderTest.java
index ee4d43131..84fe9d94d 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/util/id3reader/ChapterReaderTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/util/id3reader/ChapterReaderTest.java
@@ -162,4 +162,27 @@ public class ChapterReaderTest {
assertEquals(EmbeddedChapterImage.makeUrl(1771, 308), chapters.get(2).getImageUrl());
assertEquals(EmbeddedChapterImage.makeUrl(2259, 308), chapters.get(3).getImageUrl());
}
+
+ @Test
+ public void testRealFileHindenburgJournalistPro() throws IOException, ID3ReaderException {
+ CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader()
+ .getResource("media-parser/hindenburg-journalist-pro.mp3").openStream());
+ ChapterReader reader = new ChapterReader(inputStream);
+ reader.readInputStream();
+ List<Chapter> chapters = reader.getChapters();
+
+ assertEquals(2, chapters.size());
+
+ assertEquals(0, chapters.get(0).getStart());
+ assertEquals(5006, chapters.get(1).getStart());
+
+ assertEquals("Chapter Marker 1", chapters.get(0).getTitle());
+ assertEquals("Chapter Marker 2", chapters.get(1).getTitle());
+
+ assertEquals("https://example.com/chapter1url", chapters.get(0).getLink());
+ assertEquals("https://example.com/chapter2url", chapters.get(1).getLink());
+
+ assertEquals(EmbeddedChapterImage.makeUrl(5330, 4015), chapters.get(0).getImageUrl());
+ assertEquals(EmbeddedChapterImage.makeUrl(9498, 4364), chapters.get(1).getImageUrl());
+ }
}
diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/playback/ExternalMediaTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/playback/ExternalMediaTest.java
deleted file mode 100644
index d5e63eeba..000000000
--- a/core/src/test/java/de/danoeh/antennapod/core/util/playback/ExternalMediaTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package de.danoeh.antennapod.core.util.playback;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.SharedPreferences;
-import androidx.preference.PreferenceManager;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import de.danoeh.antennapod.core.feed.MediaType;
-import org.junit.After;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import static org.junit.Assert.assertEquals;
-
-/**
- * Tests for {@link ExternalMedia} entity.
- */
-@RunWith(RobolectricTestRunner.class)
-public class ExternalMediaTest {
-
- private static final int NOT_SET = -1;
- private static final int POSITION = 50;
- private static final int LAST_PLAYED_TIME = 1650;
-
- @After
- public void tearDown() {
- clearSharedPrefs();
- }
-
- @SuppressLint("CommitPrefEdits")
- private void clearSharedPrefs() {
- SharedPreferences prefs = getDefaultSharedPrefs();
- SharedPreferences.Editor editor = prefs.edit();
- editor.clear();
- editor.commit();
- }
-
- private SharedPreferences getDefaultSharedPrefs() {
- Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
- return PreferenceManager.getDefaultSharedPreferences(context);
- }
-
- @Test
- public void testSaveCurrentPositionUpdatesPreferences() {
- assertEquals(NOT_SET, getDefaultSharedPrefs().getInt(ExternalMedia.PREF_POSITION, NOT_SET));
- assertEquals(NOT_SET, getDefaultSharedPrefs().getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, NOT_SET));
-
- ExternalMedia media = new ExternalMedia("source", MediaType.AUDIO);
- media.saveCurrentPosition(getDefaultSharedPrefs(), POSITION, LAST_PLAYED_TIME);
-
- assertEquals(POSITION, getDefaultSharedPrefs().getInt(ExternalMedia.PREF_POSITION, NOT_SET));
- assertEquals(LAST_PLAYED_TIME, getDefaultSharedPrefs().getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, NOT_SET));
- }
-}
diff --git a/core/src/test/resources/media-parser/hindenburg-journalist-pro.m4a b/core/src/test/resources/media-parser/hindenburg-journalist-pro.m4a
new file mode 100644
index 000000000..bd64dd9da
--- /dev/null
+++ b/core/src/test/resources/media-parser/hindenburg-journalist-pro.m4a
Binary files differ
diff --git a/core/src/test/resources/media-parser/hindenburg-journalist-pro.mp3 b/core/src/test/resources/media-parser/hindenburg-journalist-pro.mp3
new file mode 100644
index 000000000..d341b6045
--- /dev/null
+++ b/core/src/test/resources/media-parser/hindenburg-journalist-pro.mp3
Binary files differ
diff --git a/net/README.md b/net/README.md
new file mode 100644
index 000000000..4d578407c
--- /dev/null
+++ b/net/README.md
@@ -0,0 +1,3 @@
+# :net
+
+This folder contains modules that directly interact with the network.
diff --git a/net/ssl/README.md b/net/ssl/README.md
new file mode 100644
index 000000000..bf01f3ab6
--- /dev/null
+++ b/net/ssl/README.md
@@ -0,0 +1,3 @@
+# :net:ssl
+
+This module provides SSL backports and security provider implementations.
diff --git a/net/ssl/build.gradle b/net/ssl/build.gradle
new file mode 100644
index 000000000..9426b7234
--- /dev/null
+++ b/net/ssl/build.gradle
@@ -0,0 +1,65 @@
+apply plugin: "com.android.library"
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+
+ multiDexEnabled false
+
+ testApplicationId "de.danoeh.antennapod.core.tests"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile("proguard-android.txt")
+ }
+ debug {
+ // debug build has method count over 64k single-dex threshold.
+ // For building debug build to use on Android < 21 (pre-Android 5) devices,
+ // you need to manually change class
+ // de.danoeh.antennapod.PodcastApp to extend MultiDexApplication .
+ // See Issue #2813
+ multiDexEnabled true
+ }
+ }
+
+ flavorDimensions "market"
+ productFlavors {
+ free {
+ dimension "market"
+ }
+ play {
+ dimension "market"
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+
+ lintOptions {
+ disable 'GradleDependency'
+ warningsAsErrors true
+ abortOnError true
+ }
+}
+
+dependencies {
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
+
+ playImplementation "com.google.android.gms:play-services-base:$playServicesVersion"
+ freeImplementation "org.conscrypt:conscrypt-android:$conscryptVersion"
+}
diff --git a/net/ssl/src/free/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java b/net/ssl/src/free/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java
new file mode 100644
index 000000000..48b5690cc
--- /dev/null
+++ b/net/ssl/src/free/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java
@@ -0,0 +1,13 @@
+package de.danoeh.antennapod.net.ssl;
+
+import android.content.Context;
+import org.conscrypt.Conscrypt;
+
+import java.security.Security;
+
+public class SslProviderInstaller {
+ public static void install(Context context) {
+ // Insert bundled conscrypt as highest security provider (overrides OS version).
+ Security.insertProviderAt(Conscrypt.newProvider(), 1);
+ }
+}
diff --git a/net/ssl/src/main/AndroidManifest.xml b/net/ssl/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..2acf91510
--- /dev/null
+++ b/net/ssl/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.net.ssl" />
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/BackportCaCerts.java
index 78c105e38..ecfc99e15 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java
+++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/BackportCaCerts.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.ssl;
+package de.danoeh.antennapod.net.ssl;
public class BackportCaCerts {
public static final String SECTIGO_USER_TRUST = "-----BEGIN CERTIFICATE-----\n"
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/BackportTrustManager.java
index 81d2a0709..3a188b47a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java
+++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/BackportTrustManager.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.ssl;
+package de.danoeh.antennapod.net.ssl;
import android.util.Log;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/CompositeX509TrustManager.java
index 7af96a492..16b2f0931 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java
+++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/CompositeX509TrustManager.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.ssl;
+package de.danoeh.antennapod.net.ssl;
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/NoV1SslSocketFactory.java
index 96a42f22d..0e31cda68 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java
+++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/NoV1SslSocketFactory.java
@@ -1,6 +1,4 @@
-package de.danoeh.antennapod.core.ssl;
-
-import de.danoeh.antennapod.core.util.Flavors;
+package de.danoeh.antennapod.net.ssl;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
@@ -22,7 +20,7 @@ public class NoV1SslSocketFactory extends SSLSocketFactory {
try {
SSLContext sslContext;
- if (Flavors.FLAVOR == Flavors.FREE) {
+ if (BuildConfig.FLAVOR.equals("free")) {
// Free flavor (bundles modern conscrypt): support for TLSv1.3 is guaranteed.
sslContext = SSLContext.getInstance("TLSv1.3");
} else {
@@ -84,7 +82,7 @@ public class NoV1SslSocketFactory extends SSLSocketFactory {
}
private void configureSocket(SSLSocket s) {
- if (Flavors.FLAVOR == Flavors.FREE) {
+ if (BuildConfig.FLAVOR.equals("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" });
diff --git a/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/SslClientSetup.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/SslClientSetup.java
new file mode 100644
index 000000000..666010d2f
--- /dev/null
+++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/SslClientSetup.java
@@ -0,0 +1,37 @@
+package de.danoeh.antennapod.net.ssl;
+
+import android.os.Build;
+import okhttp3.CipherSuite;
+import okhttp3.ConnectionSpec;
+import okhttp3.OkHttpClient;
+
+import javax.net.ssl.X509TrustManager;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class SslClientSetup {
+ public static void installCertificates(OkHttpClient.Builder builder) {
+ if (BuildConfig.FLAVOR.equals("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());
+ cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
+ cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
+
+ ConnectionSpec legacyTls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
+ .build();
+ builder.connectionSpecs(Arrays.asList(legacyTls, ConnectionSpec.CLEARTEXT));
+ }
+ }
+}
diff --git a/net/ssl/src/play/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java b/net/ssl/src/play/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java
new file mode 100644
index 000000000..6c89df5ec
--- /dev/null
+++ b/net/ssl/src/play/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java
@@ -0,0 +1,20 @@
+package de.danoeh.antennapod.net.ssl;
+
+import android.content.Context;
+import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
+import com.google.android.gms.common.GooglePlayServicesRepairableException;
+import com.google.android.gms.security.ProviderInstaller;
+
+public class SslProviderInstaller {
+ public static void install(Context context) {
+ try {
+ ProviderInstaller.installIfNeeded(context);
+ } catch (GooglePlayServicesRepairableException e) {
+ e.printStackTrace();
+ GoogleApiAvailability.getInstance().showErrorNotification(context, e.getConnectionStatusCode());
+ } catch (GooglePlayServicesNotAvailableException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 0298c7b2a..a87de1afa 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,5 @@
include ':app'
include ':core'
+include ':net:ssl'
include ':ui:app-start-intent'
include ':ui:common'
diff --git a/ui/app-start-intent/build.gradle b/ui/app-start-intent/build.gradle
index fabd8937f..144ce72a1 100644
--- a/ui/app-start-intent/build.gradle
+++ b/ui/app-start-intent/build.gradle
@@ -39,6 +39,7 @@ android {
}
lintOptions {
+ disable 'GradleDependency'
warningsAsErrors true
abortOnError true
}
diff --git a/ui/common/build.gradle b/ui/common/build.gradle
index fabd8937f..144ce72a1 100644
--- a/ui/common/build.gradle
+++ b/ui/common/build.gradle
@@ -39,6 +39,7 @@ android {
}
lintOptions {
+ disable 'GradleDependency'
warningsAsErrors true
abortOnError true
}