Merge pull request #6153 from ByteHamster/fast-document-file
Speed up local folder refresh
This commit is contained in:
commit
cac231a461
|
@ -8,8 +8,8 @@ import android.util.Log;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
|
@ -24,7 +24,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 +49,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 +75,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 +85,10 @@ public class LocalFeedUpdater {
|
|||
feed = DBTasks.updateFeed(context, feed, false);
|
||||
|
||||
// list files in feed folder
|
||||
List<DocumentFile> mediaFiles = new ArrayList<>();
|
||||
List<FastDocumentFile> allFiles = FastDocumentFile.list(context, folderUri);
|
||||
List<FastDocumentFile> mediaFiles = new ArrayList<>();
|
||||
Set<String> 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 +121,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 +139,18 @@ public class LocalFeedUpdater {
|
|||
* Returns the image URL for the local feed.
|
||||
*/
|
||||
@NonNull
|
||||
static String getImageUrl(@NonNull DocumentFile documentFolder) {
|
||||
static String getImageUrl(List<FastDocumentFile> 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 +158,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,26 +171,36 @@ 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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,9 +226,11 @@ 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));
|
||||
Id3MetadataReader reader = new Id3MetadataReader(
|
||||
new CountingInputStream(new BufferedInputStream(inputStream)));
|
||||
reader.readInputStream();
|
||||
item.setDescriptionIfLonger(reader.getComment());
|
||||
} catch (IOException | ID3ReaderException e) {
|
||||
|
|
|
@ -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<Chapter> readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException {
|
||||
VorbisCommentChapterReader reader = new VorbisCommentChapterReader(input);
|
||||
VorbisCommentChapterReader reader = new VorbisCommentChapterReader(new BufferedInputStream(input));
|
||||
reader.readInputStream();
|
||||
List<Chapter> chapters = reader.getChapters();
|
||||
if (chapters == null) {
|
||||
|
|
|
@ -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<FastDocumentFile> 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<FastDocumentFile> 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* <p>Wraps an Android assets file or folder as a DocumentFile object.</p>
|
||||
*
|
||||
* <p>This is used to emulate access to the external storage.</p>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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<FeedItem> 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<FastDocumentFile> 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<FastDocumentFile> 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<FastDocumentFile> 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<FastDocumentFile> 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<FastDocumentFile> 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<FastDocumentFile> 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<DocumentFile> dfMock = Mockito.mockStatic(DocumentFile.class)) {
|
||||
try (MockedStatic<FastDocumentFile> 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<FastDocumentFile> mockLocalFolder(String folderName) {
|
||||
List<FastDocumentFile> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue