From 25dd4902ba47d60b854e329fc8344588b3a03977 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 19 Oct 2022 20:15:09 +0200 Subject: [PATCH 1/3] Make loading DocumentFiles faster --- .../core/feed/LocalFeedUpdater.java | 63 ++++---- .../core/util/FastDocumentFile.java | 77 ++++++++++ .../provider/AssetsDocumentFile.java | 138 ------------------ .../core/feed/LocalFeedUpdaterTest.java | 98 +++++-------- 4 files changed, 150 insertions(+), 226 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/FastDocumentFile.java delete mode 100644 core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index d37bc230d..e8bf3d495 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -8,7 +8,6 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.documentfile.provider.DocumentFile; import java.io.IOException; import java.io.InputStream; @@ -24,7 +23,10 @@ import java.util.Locale; import java.util.Set; import java.util.UUID; +import androidx.annotation.VisibleForTesting; +import androidx.documentfile.provider.DocumentFile; import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.util.FastDocumentFile; import de.danoeh.antennapod.model.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; @@ -46,12 +48,22 @@ import org.apache.commons.io.input.CountingInputStream; public class LocalFeedUpdater { private static final String TAG = "LocalFeedUpdater"; - static final String[] PREFERRED_FEED_IMAGE_FILENAMES = { "folder.jpg", "Folder.jpg", "folder.png", "Folder.png" }; + static final String[] PREFERRED_FEED_IMAGE_FILENAMES = {"folder.jpg", "Folder.jpg", "folder.png", "Folder.png"}; public static void updateFeed(Feed feed, Context context, @Nullable UpdaterProgressListener updaterProgressListener) { try { - tryUpdateFeed(feed, context, updaterProgressListener); + String uriString = feed.getDownload_url().replace(Feed.PREFIX_LOCAL_FOLDER, ""); + DocumentFile documentFolder = DocumentFile.fromTreeUri(context, Uri.parse(uriString)); + if (documentFolder == null) { + throw new IOException("Unable to retrieve document tree. " + + "Try re-connecting the folder on the podcast info page."); + } + if (!documentFolder.exists() || !documentFolder.canRead()) { + throw new IOException("Cannot read local directory. " + + "Try re-connecting the folder on the podcast info page."); + } + tryUpdateFeed(feed, context, documentFolder.getUri(), updaterProgressListener); if (mustReportDownloadSuccessful(feed)) { reportSuccess(feed); @@ -62,19 +74,9 @@ public class LocalFeedUpdater { } } - private static void tryUpdateFeed(Feed feed, Context context, UpdaterProgressListener updaterProgressListener) - throws IOException { - String uriString = feed.getDownload_url().replace(Feed.PREFIX_LOCAL_FOLDER, ""); - DocumentFile documentFolder = DocumentFile.fromTreeUri(context, Uri.parse(uriString)); - if (documentFolder == null) { - throw new IOException("Unable to retrieve document tree. " - + "Try re-connecting the folder on the podcast info page."); - } - if (!documentFolder.exists() || !documentFolder.canRead()) { - throw new IOException("Cannot read local directory. " - + "Try re-connecting the folder on the podcast info page."); - } - + @VisibleForTesting + static void tryUpdateFeed(Feed feed, Context context, Uri folderUri, + UpdaterProgressListener updaterProgressListener) { if (feed.getItems() == null) { feed.setItems(new ArrayList<>()); } @@ -82,9 +84,10 @@ public class LocalFeedUpdater { feed = DBTasks.updateFeed(context, feed, false); // list files in feed folder - List mediaFiles = new ArrayList<>(); + List allFiles = FastDocumentFile.list(context, folderUri); + List mediaFiles = new ArrayList<>(); Set mediaFileNames = new HashSet<>(); - for (DocumentFile file : documentFolder.listFiles()) { + for (FastDocumentFile file : allFiles) { String mimeType = MimeTypeUtils.getMimeType(file.getType(), file.getUri().toString()); MediaType mediaType = MediaType.fromMimeType(mimeType); if (mediaType == MediaType.AUDIO || mediaType == MediaType.VIDEO) { @@ -117,7 +120,7 @@ public class LocalFeedUpdater { } } - feed.setImageUrl(getImageUrl(documentFolder)); + feed.setImageUrl(getImageUrl(allFiles, folderUri)); feed.getPreferences().setAutoDownload(false); feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO); @@ -135,17 +138,18 @@ public class LocalFeedUpdater { * Returns the image URL for the local feed. */ @NonNull - static String getImageUrl(@NonNull DocumentFile documentFolder) { + static String getImageUrl(List files, Uri folderUri) { // look for special file names for (String iconLocation : PREFERRED_FEED_IMAGE_FILENAMES) { - DocumentFile image = documentFolder.findFile(iconLocation); - if (image != null) { - return image.getUri().toString(); + for (FastDocumentFile file : files) { + if (iconLocation.equals(file.getName())) { + return file.getUri().toString(); + } } } // use the first image in the folder if existing - for (DocumentFile file : documentFolder.listFiles()) { + for (FastDocumentFile file : files) { String mime = file.getType(); if (mime != null && (mime.startsWith("image/jpeg") || mime.startsWith("image/png"))) { return file.getUri().toString(); @@ -153,7 +157,7 @@ public class LocalFeedUpdater { } // use default icon as fallback - return Feed.PREFIX_GENERATIVE_COVER + documentFolder.getUri(); + return Feed.PREFIX_GENERATIVE_COVER + folderUri; } private static FeedItem feedContainsFile(Feed feed, String filename) { @@ -166,12 +170,12 @@ public class LocalFeedUpdater { return null; } - private static FeedItem createFeedItem(Feed feed, DocumentFile file, Context context) { + private static FeedItem createFeedItem(Feed feed, FastDocumentFile file, Context context) { FeedItem item = new FeedItem(0, file.getName(), UUID.randomUUID().toString(), - file.getName(), new Date(file.lastModified()), FeedItem.UNPLAYED, feed); + file.getName(), new Date(file.getLastModified()), FeedItem.UNPLAYED, feed); item.disableAutoDownload(); - long size = file.length(); + long size = file.getLength(); FeedMedia media = new FeedMedia(0, item, 0, 0, size, file.getType(), file.getUri().toString(), file.getUri().toString(), false, null, 0, 0); item.setMedia(media); @@ -185,7 +189,7 @@ public class LocalFeedUpdater { return item; } - private static void loadMetadata(FeedItem item, DocumentFile file, Context context) { + private static void loadMetadata(FeedItem item, FastDocumentFile file, Context context) { MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); mediaMetadataRetriever.setDataSource(context, file.getUri()); @@ -211,6 +215,7 @@ public class LocalFeedUpdater { item.getMedia().setDuration((int) Long.parseLong(durationStr)); item.getMedia().setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null); + mediaMetadataRetriever.close(); try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) { Id3MetadataReader reader = new Id3MetadataReader(new CountingInputStream(inputStream)); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FastDocumentFile.java b/core/src/main/java/de/danoeh/antennapod/core/util/FastDocumentFile.java new file mode 100644 index 000000000..885d66687 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FastDocumentFile.java @@ -0,0 +1,77 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.DocumentsContract; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Android's DocumentFile is slow because every single method call queries the ContentResolver. + * This queries the ContentResolver a single time with all the information. + */ +public class FastDocumentFile { + private final String name; + private final String type; + private final Uri uri; + private final long length; + private final long lastModified; + + public static List list(Context context, Uri folderUri) { + if (android.os.Build.VERSION.SDK_INT < 21) { + return Collections.emptyList(); + } + + Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(folderUri, + DocumentsContract.getDocumentId(folderUri)); + Cursor cursor = context.getContentResolver().query(childrenUri, new String[] { + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_LAST_MODIFIED, + DocumentsContract.Document.COLUMN_MIME_TYPE}, null, null, null); + ArrayList list = new ArrayList<>(); + while (cursor.moveToNext()) { + String id = cursor.getString(0); + Uri uri = DocumentsContract.buildDocumentUriUsingTree(folderUri, id); + String name = cursor.getString(1); + long size = cursor.getLong(2); + long lastModified = cursor.getLong(3); + String mimeType = cursor.getString(4); + list.add(new FastDocumentFile(name, mimeType, uri, size, lastModified)); + } + cursor.close(); + return list; + } + + public FastDocumentFile(String name, String type, Uri uri, long length, long lastModified) { + this.name = name; + this.type = type; + this.uri = uri; + this.length = length; + this.lastModified = lastModified; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public Uri getUri() { + return uri; + } + + public long getLength() { + return length; + } + + public long getLastModified() { + return lastModified; + } +} diff --git a/core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java b/core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java deleted file mode 100644 index 8a8205c10..000000000 --- a/core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java +++ /dev/null @@ -1,138 +0,0 @@ -package androidx.documentfile.provider; - -import android.content.res.AssetManager; -import android.net.Uri; -import android.webkit.MimeTypeMap; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.IOException; - -/** - *

Wraps an Android assets file or folder as a DocumentFile object.

- * - *

This is used to emulate access to the external storage.

- */ -public class AssetsDocumentFile extends DocumentFile { - - /** - * Absolute file path in the assets folder. - */ - @NonNull - private final String fileName; - - @NonNull - private final AssetManager assetManager; - - public AssetsDocumentFile(@NonNull String fileName, @NonNull AssetManager assetManager) { - super(null); - this.fileName = fileName; - this.assetManager = assetManager; - } - - @Nullable - @Override - public DocumentFile createFile(@NonNull String mimeType, @NonNull String displayName) { - return null; - } - - @Nullable - @Override - public DocumentFile createDirectory(@NonNull String displayName) { - return null; - } - - @NonNull - @Override - public Uri getUri() { - return Uri.parse(fileName); - } - - @Nullable - @Override - public String getName() { - int pos = fileName.indexOf('/'); - if (pos >= 0) { - return fileName.substring(pos + 1); - } else { - return fileName; - } - } - - @Nullable - @Override - public String getType() { - String extension = MimeTypeMap.getFileExtensionFromUrl(fileName); - return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - } - - @Override - public boolean isDirectory() { - return false; - } - - @Override - public boolean isFile() { - return true; - } - - @Override - public boolean isVirtual() { - return false; - } - - @Override - public long lastModified() { - return 0; - } - - @Override - public long length() { - return 0; - } - - @Override - public boolean canRead() { - return true; - } - - @Override - public boolean canWrite() { - return false; - } - - @Override - public boolean delete() { - return false; - } - - @Override - public boolean exists() { - return true; - } - - @NonNull - @Override - public DocumentFile[] listFiles() { - try { - String[] files = assetManager.list(fileName); - if (files == null) { - return new DocumentFile[0]; - } - DocumentFile[] result = new DocumentFile[files.length]; - for (int i = 0; i < files.length; i++) { - String subFileName = fileName + '/' + files[i]; - result[i] = new AssetsDocumentFile(subFileName, assetManager); - } - return result; - } catch (IOException e) { - return new DocumentFile[0]; - } - } - - @Override - public boolean renameTo(@NonNull String displayName) { - return false; - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java index 05b0584ed..4cf746ba4 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java @@ -7,11 +7,10 @@ import android.net.Uri; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; -import androidx.documentfile.provider.AssetsDocumentFile; -import androidx.documentfile.provider.DocumentFile; import androidx.test.platform.app.InstrumentationRegistry; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.util.FastDocumentFile; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.storage.database.PodDBAdapter; @@ -24,11 +23,12 @@ import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.shadows.ShadowMediaMetadataRetriever; -import java.io.IOException; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Objects; import de.danoeh.antennapod.core.ApplicationCallbacks; import de.danoeh.antennapod.core.ClientConfig; @@ -60,8 +60,8 @@ public class LocalFeedUpdaterTest { */ private static final String FEED_URL = "content://com.android.externalstorage.documents/tree/primary%3ADownload%2Flocal-feed"; - private static final String LOCAL_FEED_DIR1 = "local-feed1"; - private static final String LOCAL_FEED_DIR2 = "local-feed2"; + private static final String LOCAL_FEED_DIR1 = "src/test/assets/local-feed1"; + private static final String LOCAL_FEED_DIR2 = "src/test/assets/local-feed2"; private Context context; @@ -172,74 +172,61 @@ public class LocalFeedUpdaterTest { Feed feed = verifySingleFeedInDatabase(); List feedItems = DBReader.getFeedItemList(feed); - FeedItem feedItem = feedItems.get(0); - - assertEquals("track1.mp3", feedItem.getTitle()); - - Date pubDate = feedItem.getPubDate(); - Calendar calendar = GregorianCalendar.getInstance(); - calendar.setTime(pubDate); - assertEquals(2020, calendar.get(Calendar.YEAR)); - assertEquals(6 - 1, calendar.get(Calendar.MONTH)); - assertEquals(1, calendar.get(Calendar.DAY_OF_MONTH)); - assertEquals(22, calendar.get(Calendar.HOUR_OF_DAY)); - assertEquals(23, calendar.get(Calendar.MINUTE)); - assertEquals(24, calendar.get(Calendar.SECOND)); + assertEquals("track1.mp3", feedItems.get(0).getTitle()); } @Test public void testGetImageUrl_EmptyFolder() { - DocumentFile documentFolder = mockDocumentFolder(); - String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder); + String imageUrl = LocalFeedUpdater.getImageUrl(Collections.emptyList(), Uri.EMPTY); assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER)); } @Test public void testGetImageUrl_NoImageButAudioFiles() { - DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3")); - String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder); + List folder = Collections.singletonList(mockDocumentFile("audio.mp3", "audio/mp3")); + String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER)); } @Test public void testGetImageUrl_PreferredImagesFilenames() { for (String filename : LocalFeedUpdater.PREFERRED_FEED_IMAGE_FILENAMES) { - DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"), + List folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"), mockDocumentFile(filename, "image/jpeg")); // image MIME type doesn't matter - String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder); + String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); assertThat(imageUrl, endsWith(filename)); } } @Test public void testGetImageUrl_OtherImageFilenameJpg() { - DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"), + List folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"), mockDocumentFile("my-image.jpg", "image/jpeg")); - String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder); + String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); assertThat(imageUrl, endsWith("my-image.jpg")); } @Test public void testGetImageUrl_OtherImageFilenameJpeg() { - DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"), + List folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"), mockDocumentFile("my-image.jpeg", "image/jpeg")); - String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder); + String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); assertThat(imageUrl, endsWith("my-image.jpeg")); } @Test public void testGetImageUrl_OtherImageFilenamePng() { - DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"), + List folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"), mockDocumentFile("my-image.png", "image/png")); - String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder); + String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); assertThat(imageUrl, endsWith("my-image.png")); } @Test public void testGetImageUrl_OtherImageFilenameUnsupportedMimeType() { - DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"), + List folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"), mockDocumentFile("my-image.svg", "image/svg+xml")); - String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder); + String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER)); } @@ -248,9 +235,8 @@ public class LocalFeedUpdaterTest { * * @param localFeedDir assets local feed folder with media files */ - private void mapDummyMetadata(@NonNull String localFeedDir) throws IOException { - String[] fileNames = context.getAssets().list(localFeedDir); - for (String fileName : fileNames) { + private void mapDummyMetadata(@NonNull String localFeedDir) { + for (String fileName : Objects.requireNonNull(new File(localFeedDir).list())) { String path = localFeedDir + '/' + fileName; ShadowMediaMetadataRetriever.addMetadata(path, MediaMetadataRetriever.METADATA_KEY_DURATION, "10"); @@ -259,24 +245,21 @@ public class LocalFeedUpdaterTest { ShadowMediaMetadataRetriever.addMetadata(path, MediaMetadataRetriever.METADATA_KEY_DATE, "20200601T222324"); } - } /** - * Calls the method {@link LocalFeedUpdater#updateFeed(Feed, Context)} with - * the given local feed folder. + * Calls the method LocalFeedUpdater#tryUpdateFeed with the given local feed folder. * * @param localFeedDir assets local feed folder with media files */ private void callUpdateFeed(@NonNull String localFeedDir) { - DocumentFile documentFile = new AssetsDocumentFile(localFeedDir, context.getAssets()); - try (MockedStatic dfMock = Mockito.mockStatic(DocumentFile.class)) { + try (MockedStatic dfMock = Mockito.mockStatic(FastDocumentFile.class)) { // mock external storage - dfMock.when(() -> DocumentFile.fromTreeUri(any(), any())).thenReturn(documentFile); + dfMock.when(() -> FastDocumentFile.list(any(), any())).thenReturn(mockLocalFolder(localFeedDir)); // call method to test Feed feed = new Feed(FEED_URL, null); - LocalFeedUpdater.updateFeed(feed, context, null); + LocalFeedUpdater.tryUpdateFeed(feed, context, null, null); } } @@ -306,21 +289,18 @@ public class LocalFeedUpdaterTest { * Create a DocumentFile mock object. */ @NonNull - private static DocumentFile mockDocumentFile(@NonNull String fileName, @NonNull String mimeType) { - DocumentFile file = mock(DocumentFile.class); - when(file.getName()).thenReturn(fileName); - when(file.getUri()).thenReturn(Uri.parse("file:///path/" + fileName)); - when(file.getType()).thenReturn(mimeType); - return file; + private static FastDocumentFile mockDocumentFile(@NonNull String fileName, @NonNull String mimeType) { + return new FastDocumentFile(fileName, mimeType, Uri.parse("file:///path/" + fileName), 0, 0); } - /** - * Create a DocumentFile folder mock object with a list of files. - */ - @NonNull - private static DocumentFile mockDocumentFolder(DocumentFile... files) { - DocumentFile documentFolder = mock(DocumentFile.class); - when(documentFolder.listFiles()).thenReturn(files); - return documentFolder; + private static List mockLocalFolder(String folderName) { + List files = new ArrayList<>(); + for (File f : Objects.requireNonNull(new File(folderName).listFiles())) { + String extension = MimeTypeMap.getFileExtensionFromUrl(f.getPath()); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + files.add(new FastDocumentFile(f.getName(), mimeType, + Uri.parse(f.toURI().toString()), f.length(), f.lastModified())); + } + return files; } } From 9624d8ce9e5e5dc4df6330197e3294a079e3eb94 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 19 Oct 2022 20:29:29 +0200 Subject: [PATCH 2/3] Speed up chapter parsing --- .../antennapod/core/feed/LocalFeedUpdater.java | 4 +++- .../danoeh/antennapod/core/util/ChapterUtils.java | 9 +++++---- .../antennapod/parser/media/id3/ID3Reader.java | 13 ++++++------- .../parser/media/id3/model/FrameHeader.java | 11 ----------- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index e8bf3d495..742df3f14 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -9,6 +9,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; @@ -218,7 +219,8 @@ public class LocalFeedUpdater { mediaMetadataRetriever.close(); try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) { - Id3MetadataReader reader = new Id3MetadataReader(new CountingInputStream(inputStream)); + Id3MetadataReader reader = new Id3MetadataReader( + new CountingInputStream(new BufferedInputStream(inputStream))); reader.readInputStream(); item.setDescriptionIfLonger(reader.getComment()); } catch (IOException | ID3ReaderException e) { 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 723ea1d47..cc63bf2b0 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 @@ -23,6 +23,7 @@ import okhttp3.Request; import okhttp3.Response; import org.apache.commons.io.input.CountingInputStream; +import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -120,17 +121,17 @@ public class ChapterUtils { if (!source.exists()) { throw new IOException("Local file does not exist"); } - return new CountingInputStream(new FileInputStream(source)); + return new CountingInputStream(new BufferedInputStream(new FileInputStream(source))); } else if (playable.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) { Uri uri = Uri.parse(playable.getStreamUrl()); - return new CountingInputStream(context.getContentResolver().openInputStream(uri)); + return new CountingInputStream(new BufferedInputStream(context.getContentResolver().openInputStream(uri))); } else { Request request = new Request.Builder().url(playable.getStreamUrl()).build(); Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute(); if (response.body() == null) { throw new IOException("Body is null"); } - return new CountingInputStream(response.body().byteStream()); + return new CountingInputStream(new BufferedInputStream(response.body().byteStream())); } } @@ -171,7 +172,7 @@ public class ChapterUtils { @NonNull private static List readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException { - VorbisCommentChapterReader reader = new VorbisCommentChapterReader(input); + VorbisCommentChapterReader reader = new VorbisCommentChapterReader(new BufferedInputStream(input)); reader.readInputStream(); List chapters = reader.getChapters(); if (chapters == null) { diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ID3Reader.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ID3Reader.java index baaff2752..a791baf66 100644 --- a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ID3Reader.java +++ b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ID3Reader.java @@ -46,7 +46,7 @@ public class ID3Reader { } protected void readFrame(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException { - Log.d(TAG, "Skipping frame: " + frameHeader.toString()); + Log.d(TAG, "Skipping frame: " + frameHeader.getId() + ", size: " + frameHeader.getSize()); skipBytes(frameHeader.getSize()); } @@ -106,7 +106,7 @@ public class ID3Reader { @NonNull FrameHeader readFrameHeader() throws IOException { - String id = readIsoStringFixed(FRAME_ID_LENGTH); + String id = readPlainBytesToString(FRAME_ID_LENGTH); int size = readInt(); if (tagHeader != null && tagHeader.getVersion() >= 0x0400) { size = unsynchsafe(size); @@ -136,15 +136,14 @@ public class ID3Reader { return readEncodedString(encoding, max - 1); } - @SuppressWarnings("CharsetObjectCanBeUsed") - protected String readIsoStringFixed(int length) throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + protected String readPlainBytesToString(int length) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); int bytesRead = 0; while (bytesRead < length) { - bytes.write(readByte()); + stringBuilder.append((char) readByte()); bytesRead++; } - return Charset.forName("ISO-8859-1").newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString(); + return stringBuilder.toString(); } protected String readIsoStringNullTerminated(int max) throws IOException { diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/model/FrameHeader.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/model/FrameHeader.java index 1a298a0df..3dd19008c 100644 --- a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/model/FrameHeader.java +++ b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/model/FrameHeader.java @@ -1,18 +1,7 @@ package de.danoeh.antennapod.parser.media.id3.model; -import androidx.annotation.NonNull; - public class FrameHeader extends Header { - private final short flags; - public FrameHeader(String id, int size, short flags) { super(id, size); - this.flags = flags; - } - - @Override - @NonNull - public String toString() { - return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, size); } } From c7e41c31b61e830eabf4df6eeb06d86d36939d97 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Fri, 21 Oct 2022 22:01:47 +0200 Subject: [PATCH 3/3] If file size and name are the same, just assume that the metadata is the same as well --- .../antennapod/core/feed/LocalFeedUpdater.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index 742df3f14..8ee924243 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -181,12 +181,22 @@ public class LocalFeedUpdater { file.getUri().toString(), file.getUri().toString(), false, null, 0, 0); item.setMedia(media); + for (FeedItem existingItem : feed.getItems()) { + if (existingItem.getMedia() != null + && existingItem.getMedia().getDownload_url().equals(file.getUri().toString()) + && file.getLength() == existingItem.getMedia().getSize()) { + // We found an old file that we already scanned. Re-use metadata. + item.updateFromOther(existingItem); + return item; + } + } + + // Did not find existing item. Scan metadata. try { loadMetadata(item, file, context); } catch (Exception e) { item.setDescriptionIfLonger(e.getMessage()); } - return item; }