diff options
Diffstat (limited to 'core/src/main/java/de/danoeh')
8 files changed, 187 insertions, 42 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java index 8c80e9151..a0ec16578 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java @@ -4,7 +4,6 @@ import android.content.ContentResolver; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; @@ -77,7 +76,7 @@ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> { @Nullable @Override public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) { - return new LoadData<>(new ObjectKey(model), new OkHttpStreamFetcher(client, new GlideUrl(model))); + return new LoadData<>(new ObjectKey(model), new ResizingOkHttpStreamFetcher(client, new GlideUrl(model))); } @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java new file mode 100644 index 000000000..7b8eed6e0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java @@ -0,0 +1,132 @@ +package de.danoeh.antennapod.core.glide; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.bumptech.glide.Priority; +import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher; +import com.bumptech.glide.load.model.GlideUrl; +import com.google.android.exoplayer2.util.Log; +import okhttp3.Call; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +public class ResizingOkHttpStreamFetcher extends OkHttpStreamFetcher { + private static final String TAG = "ResizingOkHttpStreamFetcher"; + private static final int MAX_DIMENSIONS = 2000; + private static final int MAX_FILE_SIZE = 1024 * 1024; // 1 MB + + private FileInputStream stream; + private File tempIn; + private File tempOut; + + public ResizingOkHttpStreamFetcher(Call.Factory client, GlideUrl url) { + super(client, url); + } + + @Override + public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) { + super.loadData(priority, new DataCallback<InputStream>() { + @Override + public void onDataReady(@Nullable InputStream data) { + if (data == null) { + callback.onDataReady(null); + return; + } + try { + tempIn = File.createTempFile("resize_", null); + tempOut = File.createTempFile("resize_", null); + OutputStream outputStream = new FileOutputStream(tempIn); + IOUtils.copy(data, outputStream); + outputStream.close(); + IOUtils.closeQuietly(data); + + if (tempIn.length() <= MAX_FILE_SIZE) { + try { + stream = new FileInputStream(tempIn); + callback.onDataReady(stream); // Just deliver the original, non-scaled image + } catch (FileNotFoundException fileNotFoundException) { + callback.onLoadFailed(fileNotFoundException); + } + return; + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + FileInputStream in = new FileInputStream(tempIn); + BitmapFactory.decodeStream(in, null, options); + IOUtils.closeQuietly(in); + + if (Math.max(options.outHeight, options.outWidth) >= MAX_DIMENSIONS) { + double sampleSize = (double) Math.max(options.outHeight, options.outWidth) / MAX_DIMENSIONS; + options.inSampleSize = (int) Math.pow(2d, Math.floor(Math.log(sampleSize) / Math.log(2d))); + } + + options.inJustDecodeBounds = false; + in = new FileInputStream(tempIn); + Bitmap bitmap = BitmapFactory.decodeStream(in, null, options); + IOUtils.closeQuietly(in); + + Bitmap.CompressFormat format = Build.VERSION.SDK_INT < 30 + ? Bitmap.CompressFormat.WEBP : Bitmap.CompressFormat.WEBP_LOSSY; + + int quality = 100; + while (true) { + FileOutputStream out = new FileOutputStream(tempOut); + bitmap.compress(format, quality, out); + IOUtils.closeQuietly(out); + + if (tempOut.length() > 3 * MAX_FILE_SIZE && quality >= 45) { + quality -= 40; + } else if (tempOut.length() > 2 * MAX_FILE_SIZE && quality >= 25) { + quality -= 20; + } else if (tempOut.length() > MAX_FILE_SIZE && quality >= 15) { + quality -= 10; + } else if (tempOut.length() > MAX_FILE_SIZE && quality >= 10) { + quality -= 5; + } else { + break; + } + } + + stream = new FileInputStream(tempOut); + callback.onDataReady(stream); + Log.d(TAG, "Compressed image from " + tempIn.length() / 1024 + + " to " + tempOut.length() / 1024 + " kB (quality: " + quality + "%)"); + } catch (Exception e) { + e.printStackTrace(); + + try { + stream = new FileInputStream(tempIn); + callback.onDataReady(stream); // Just deliver the original, non-scaled image + } catch (FileNotFoundException fileNotFoundException) { + e.printStackTrace(); + callback.onLoadFailed(fileNotFoundException); + } + } + } + + @Override + public void onLoadFailed(@NonNull Exception e) { + callback.onLoadFailed(e); + } + }); + } + + @Override + public void cleanup() { + IOUtils.closeQuietly(stream); + FileUtils.deleteQuietly(tempIn); + FileUtils.deleteQuietly(tempOut); + super.cleanup(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java index 3016b96d5..f74e3b9ad 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java @@ -947,14 +947,10 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { // is an episode in the queue left. // Start playback immediately if continuous playback is enabled nextMedia = callback.getNextInQueue(currentMedia); - - boolean playNextEpisode = isPlaying && - nextMedia != null && - UserPreferences.isFollowQueue(); - + boolean playNextEpisode = isPlaying && nextMedia != null; if (playNextEpisode) { Log.d(TAG, "Playback of next episode will start immediately."); - } else if (nextMedia == null){ + } else if (nextMedia == null) { Log.d(TAG, "No more episodes available to play"); } else { Log.d(TAG, "Loading next episode, but not playing automatically."); 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 2da06e226..c38d28380 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 @@ -736,7 +736,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { mediaPlayer.playMediaObject(playable, PlaybackPreferences.getCurrentEpisodeIsStream(), true, true); stateManager.validStartCommandWasReceived(); - PlaybackService.this.updateMediaSessionMetadata(playable); + updateNotificationAndMediaSession(playable); addPlayableToQueue(playable); }, error -> { Log.d(TAG, "Playable was not loaded from preferences. Stopping service."); @@ -757,7 +757,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { public void notifyVideoSurfaceAbandoned() { mediaPlayer.pause(true, false); mediaPlayer.resetVideoSurface(); - setupNotification(getPlayable()); + updateNotificationAndMediaSession(getPlayable()); stateManager.stopForeground(!UserPreferences.isPersistNotify()); } @@ -813,7 +813,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { case INITIALIZED: PlaybackPreferences.writeMediaPlaying(mediaPlayer.getPSMPInfo().playable, mediaPlayer.getPSMPInfo().playerStatus, mediaPlayer.isStreaming()); - setupNotification(newInfo); + updateNotificationAndMediaSession(newInfo.playable); break; case PREPARED: @@ -825,7 +825,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { // do not remove notification on pause based on user pref and whether android version supports expanded notifications // Change [Play] button to [Pause] - setupNotification(newInfo); + updateNotificationAndMediaSession(newInfo.playable); } else if (!UserPreferences.isPersistNotify() && !isCasting) { // remove notification on pause stateManager.stopForeground(true); @@ -841,7 +841,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { case PLAYING: PlaybackPreferences.writePlayerStatus(mediaPlayer.getPlayerStatus()); - setupNotification(newInfo); + updateNotificationAndMediaSession(newInfo.playable); setupPositionObserver(); stateManager.validStartCommandWasReceived(); // set sleep timer if auto-enabled @@ -868,7 +868,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void shouldStop() { - setupNotification(getPlayable()); // Stops foreground if not playing + updateNotificationAndMediaSession(getPlayable()); // Stops foreground if not playing } @Override @@ -887,7 +887,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { if (reloadUI) { sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); } - PlaybackService.this.updateMediaSessionMetadata(getPlayable()); + updateNotificationAndMediaSession(getPlayable()); } @Override @@ -905,8 +905,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { // Playable is being streamed and does not have a duration specified in the feed playable.setDuration(mediaPlayer.getDuration()); DBWriter.setFeedMedia((FeedMedia) playable); - updateMediaSessionMetadata(playable); - setupNotification(playable); + updateNotificationAndMediaSession(playable); } return true; @@ -1000,6 +999,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { return null; } + if (!UserPreferences.isFollowQueue()) { + Log.d(TAG, "getNextInQueue(), but follow queue is not enabled."); + updateNotificationAndMediaSession(nextItem.getMedia()); + return null; + } + if (!nextItem.getMedia().localFileAvailable() && !NetworkUtils.isStreamingAllowed() && UserPreferences.isFollowQueue() && !nextItem.getFeed().isLocalFeed()) { displayStreamingNotAllowedNotification( @@ -1273,6 +1278,11 @@ public class PlaybackService extends MediaBrowserServiceCompat { (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)); } + private void updateNotificationAndMediaSession(final Playable p) { + updateMediaSessionMetadata(p); + setupNotification(p); + } + private void updateMediaSessionMetadata(final Playable p) { if (p == null || mediaSession == null) { return; @@ -1314,10 +1324,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { /** * Prepares notification and starts the service in the foreground. */ - private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { - setupNotification(info.playable); - } - private synchronized void setupNotification(final Playable playable) { Log.d(TAG, "setupNotification"); if (playableIconLoaderThread != null) { @@ -1898,7 +1904,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { private final SharedPreferences.OnSharedPreferenceChangeListener prefListener = (sharedPreferences, key) -> { if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) { - updateMediaSessionMetadata(getPlayable()); + updateNotificationAndMediaSession(getPlayable()); } else { flavorHelper.onSharedPreference(key); } @@ -1961,7 +1967,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info) { if (connected) { - PlaybackService.this.setupNotification(info); + PlaybackService.this.updateNotificationAndMediaSession(info.playable); } else { PlayerStatus status = info.playerStatus; if ((status == PlayerStatus.PLAYING || @@ -1969,7 +1975,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { status == PlayerStatus.PREPARING || UserPreferences.isPersistNotify()) && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - PlaybackService.this.setupNotification(info); + PlaybackService.this.updateNotificationAndMediaSession(info.playable); } else if (!UserPreferences.isPersistNotify()) { stateManager.stopForeground(true); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java index 348ffaa60..3dba0735d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java @@ -47,8 +47,8 @@ public class NSMedia extends Namespace { String medium = attributes.getValue(MEDIUM); boolean validTypeMedia = false; boolean validTypeImage = false; - boolean isDefault = "true".equals(defaultStr); + String guessedType = SyndTypeUtils.getMimeTypeFromUrl(url); if (MEDIUM_AUDIO.equals(medium)) { validTypeMedia = true; @@ -56,12 +56,14 @@ public class NSMedia extends Namespace { } else if (MEDIUM_VIDEO.equals(medium)) { validTypeMedia = true; type = "video/*"; - } else if (MEDIUM_IMAGE.equals(medium)) { + } else if (MEDIUM_IMAGE.equals(medium) && (guessedType == null + || (!guessedType.startsWith("audio/") && !guessedType.startsWith("video/")))) { + // Apparently, some publishers explicitly specify the audio file as an image validTypeImage = true; type = "image/*"; } else { if (type == null) { - type = SyndTypeUtils.getMimeTypeFromUrl(url); + type = guessedType; } if (SyndTypeUtils.enclosureTypeValid(type)) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java index cb7db1709..1ac068307 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java @@ -5,9 +5,10 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import android.util.Log; -import de.danoeh.antennapod.core.BuildConfig; import okhttp3.HttpUrl; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; @@ -28,6 +29,7 @@ public final class URLChecker { private static final String TAG = "URLChecker"; private static final String AP_SUBSCRIBE = "antennapod-subscribe://"; + private static final String AP_SUBSCRIBE_DEEPLINK = "antennapod.org/deeplink/subscribe?url="; /** * Checks if URL is valid and modifies it if necessary. @@ -39,22 +41,30 @@ public final class URLChecker { url = url.trim(); String lowerCaseUrl = url.toLowerCase(); // protocol names are case insensitive if (lowerCaseUrl.startsWith("feed://")) { - if (BuildConfig.DEBUG) Log.d(TAG, "Replacing feed:// with http://"); + Log.d(TAG, "Replacing feed:// with http://"); return prepareURL(url.substring("feed://".length())); } else if (lowerCaseUrl.startsWith("pcast://")) { - if (BuildConfig.DEBUG) Log.d(TAG, "Removing pcast://"); + Log.d(TAG, "Removing pcast://"); return prepareURL(url.substring("pcast://".length())); } else if (lowerCaseUrl.startsWith("pcast:")) { - if (BuildConfig.DEBUG) Log.d(TAG, "Removing pcast:"); + Log.d(TAG, "Removing pcast:"); return prepareURL(url.substring("pcast:".length())); } else if (lowerCaseUrl.startsWith("itpc")) { - if (BuildConfig.DEBUG) Log.d(TAG, "Replacing itpc:// with http://"); + Log.d(TAG, "Replacing itpc:// with http://"); return prepareURL(url.substring("itpc://".length())); } else if (lowerCaseUrl.startsWith(AP_SUBSCRIBE)) { - if (BuildConfig.DEBUG) Log.d(TAG, "Removing antennapod-subscribe://"); + Log.d(TAG, "Removing antennapod-subscribe://"); return prepareURL(url.substring(AP_SUBSCRIBE.length())); + } else if (lowerCaseUrl.contains(AP_SUBSCRIBE_DEEPLINK)) { + Log.d(TAG, "Removing " + AP_SUBSCRIBE_DEEPLINK); + String removedWebsite = url.substring(url.indexOf(AP_SUBSCRIBE_DEEPLINK) + AP_SUBSCRIBE_DEEPLINK.length()); + try { + return prepareURL(URLDecoder.decode(removedWebsite, "UTF-8")); + } catch (UnsupportedEncodingException e) { + return prepareURL(removedWebsite); + } } else if (!(lowerCaseUrl.startsWith("http://") || lowerCaseUrl.startsWith("https://"))) { - if (BuildConfig.DEBUG) Log.d(TAG, "Adding http:// at the beginning of the URL"); + Log.d(TAG, "Adding http:// at the beginning of the URL"); return "http://" + url; } else { return url; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java index 17313ca14..b8ec3524b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java @@ -96,6 +96,10 @@ public class ID3Reader { short version = readShort(); byte flags = readByte(); int size = unsynchsafe(readInt()); + if ((flags & 0b01000000) != 0) { + int extendedHeaderSize = readInt(); + skipBytes(extendedHeaderSize - 4); + } return new TagHeader("ID3", size, version, flags); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index cd69147a6..7f4c1ceaf 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -274,13 +274,13 @@ public abstract class PlaybackController { */ private void handleStatus() { Log.d(TAG, "status: " + status.toString()); + checkMediaInfoLoaded(); switch (status) { case ERROR: EventBus.getDefault().post(new MessageEvent(activity.getString(R.string.player_error_msg))); handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN); break; case PAUSED: - checkMediaInfoLoaded(); onPositionObserverUpdate(); updatePlayButtonShowsPlay(true); if (!PlaybackService.isCasting() && PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { @@ -288,7 +288,6 @@ public abstract class PlaybackController { } break; case PLAYING: - checkMediaInfoLoaded(); if (!PlaybackService.isCasting() && PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { onAwaitingVideoSurface(); setScreenOn(true); @@ -296,26 +295,23 @@ public abstract class PlaybackController { updatePlayButtonShowsPlay(false); break; case PREPARING: - checkMediaInfoLoaded(); if (playbackService != null) { updatePlayButtonShowsPlay(!playbackService.isStartWhenPrepared()); } break; - case STOPPED: - updatePlayButtonShowsPlay(true); - break; case PREPARED: - checkMediaInfoLoaded(); updatePlayButtonShowsPlay(true); onPositionObserverUpdate(); break; case SEEKING: onPositionObserverUpdate(); break; + case STOPPED: // Fall-through case INITIALIZED: - checkMediaInfoLoaded(); updatePlayButtonShowsPlay(true); break; + default: + break; } } |