diff options
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 Binary files differnew file mode 100644 index 000000000..bd64dd9da --- /dev/null +++ b/core/src/test/resources/media-parser/hindenburg-journalist-pro.m4a diff --git a/core/src/test/resources/media-parser/hindenburg-journalist-pro.mp3 b/core/src/test/resources/media-parser/hindenburg-journalist-pro.mp3 Binary files differnew file mode 100644 index 000000000..d341b6045 --- /dev/null +++ b/core/src/test/resources/media-parser/hindenburg-journalist-pro.mp3 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 } |