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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
|
@ -24,7 +24,10 @@ import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import de.danoeh.antennapod.core.R;
|
import de.danoeh.antennapod.core.R;
|
||||||
|
import de.danoeh.antennapod.core.util.FastDocumentFile;
|
||||||
import de.danoeh.antennapod.model.download.DownloadStatus;
|
import de.danoeh.antennapod.model.download.DownloadStatus;
|
||||||
import de.danoeh.antennapod.core.storage.DBReader;
|
import de.danoeh.antennapod.core.storage.DBReader;
|
||||||
import de.danoeh.antennapod.core.storage.DBTasks;
|
import de.danoeh.antennapod.core.storage.DBTasks;
|
||||||
|
@ -46,12 +49,22 @@ import org.apache.commons.io.input.CountingInputStream;
|
||||||
public class LocalFeedUpdater {
|
public class LocalFeedUpdater {
|
||||||
private static final String TAG = "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,
|
public static void updateFeed(Feed feed, Context context,
|
||||||
@Nullable UpdaterProgressListener updaterProgressListener) {
|
@Nullable UpdaterProgressListener updaterProgressListener) {
|
||||||
try {
|
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)) {
|
if (mustReportDownloadSuccessful(feed)) {
|
||||||
reportSuccess(feed);
|
reportSuccess(feed);
|
||||||
|
@ -62,19 +75,9 @@ public class LocalFeedUpdater {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void tryUpdateFeed(Feed feed, Context context, UpdaterProgressListener updaterProgressListener)
|
@VisibleForTesting
|
||||||
throws IOException {
|
static void tryUpdateFeed(Feed feed, Context context, Uri folderUri,
|
||||||
String uriString = feed.getDownload_url().replace(Feed.PREFIX_LOCAL_FOLDER, "");
|
UpdaterProgressListener updaterProgressListener) {
|
||||||
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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (feed.getItems() == null) {
|
if (feed.getItems() == null) {
|
||||||
feed.setItems(new ArrayList<>());
|
feed.setItems(new ArrayList<>());
|
||||||
}
|
}
|
||||||
|
@ -82,9 +85,10 @@ public class LocalFeedUpdater {
|
||||||
feed = DBTasks.updateFeed(context, feed, false);
|
feed = DBTasks.updateFeed(context, feed, false);
|
||||||
|
|
||||||
// list files in feed folder
|
// 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<>();
|
Set<String> mediaFileNames = new HashSet<>();
|
||||||
for (DocumentFile file : documentFolder.listFiles()) {
|
for (FastDocumentFile file : allFiles) {
|
||||||
String mimeType = MimeTypeUtils.getMimeType(file.getType(), file.getUri().toString());
|
String mimeType = MimeTypeUtils.getMimeType(file.getType(), file.getUri().toString());
|
||||||
MediaType mediaType = MediaType.fromMimeType(mimeType);
|
MediaType mediaType = MediaType.fromMimeType(mimeType);
|
||||||
if (mediaType == MediaType.AUDIO || mediaType == MediaType.VIDEO) {
|
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().setAutoDownload(false);
|
||||||
feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO);
|
feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO);
|
||||||
|
@ -135,17 +139,18 @@ public class LocalFeedUpdater {
|
||||||
* Returns the image URL for the local feed.
|
* Returns the image URL for the local feed.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
static String getImageUrl(@NonNull DocumentFile documentFolder) {
|
static String getImageUrl(List<FastDocumentFile> files, Uri folderUri) {
|
||||||
// look for special file names
|
// look for special file names
|
||||||
for (String iconLocation : PREFERRED_FEED_IMAGE_FILENAMES) {
|
for (String iconLocation : PREFERRED_FEED_IMAGE_FILENAMES) {
|
||||||
DocumentFile image = documentFolder.findFile(iconLocation);
|
for (FastDocumentFile file : files) {
|
||||||
if (image != null) {
|
if (iconLocation.equals(file.getName())) {
|
||||||
return image.getUri().toString();
|
return file.getUri().toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// use the first image in the folder if existing
|
// use the first image in the folder if existing
|
||||||
for (DocumentFile file : documentFolder.listFiles()) {
|
for (FastDocumentFile file : files) {
|
||||||
String mime = file.getType();
|
String mime = file.getType();
|
||||||
if (mime != null && (mime.startsWith("image/jpeg") || mime.startsWith("image/png"))) {
|
if (mime != null && (mime.startsWith("image/jpeg") || mime.startsWith("image/png"))) {
|
||||||
return file.getUri().toString();
|
return file.getUri().toString();
|
||||||
|
@ -153,7 +158,7 @@ public class LocalFeedUpdater {
|
||||||
}
|
}
|
||||||
|
|
||||||
// use default icon as fallback
|
// 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) {
|
private static FeedItem feedContainsFile(Feed feed, String filename) {
|
||||||
|
@ -166,26 +171,36 @@ public class LocalFeedUpdater {
|
||||||
return null;
|
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(),
|
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();
|
item.disableAutoDownload();
|
||||||
|
|
||||||
long size = file.length();
|
long size = file.getLength();
|
||||||
FeedMedia media = new FeedMedia(0, item, 0, 0, size, file.getType(),
|
FeedMedia media = new FeedMedia(0, item, 0, 0, size, file.getType(),
|
||||||
file.getUri().toString(), file.getUri().toString(), false, null, 0, 0);
|
file.getUri().toString(), file.getUri().toString(), false, null, 0, 0);
|
||||||
item.setMedia(media);
|
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 {
|
try {
|
||||||
loadMetadata(item, file, context);
|
loadMetadata(item, file, context);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
item.setDescriptionIfLonger(e.getMessage());
|
item.setDescriptionIfLonger(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
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 mediaMetadataRetriever = new MediaMetadataRetriever();
|
||||||
mediaMetadataRetriever.setDataSource(context, file.getUri());
|
mediaMetadataRetriever.setDataSource(context, file.getUri());
|
||||||
|
|
||||||
|
@ -211,9 +226,11 @@ public class LocalFeedUpdater {
|
||||||
item.getMedia().setDuration((int) Long.parseLong(durationStr));
|
item.getMedia().setDuration((int) Long.parseLong(durationStr));
|
||||||
|
|
||||||
item.getMedia().setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null);
|
item.getMedia().setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null);
|
||||||
|
mediaMetadataRetriever.close();
|
||||||
|
|
||||||
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
|
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();
|
reader.readInputStream();
|
||||||
item.setDescriptionIfLonger(reader.getComment());
|
item.setDescriptionIfLonger(reader.getComment());
|
||||||
} catch (IOException | ID3ReaderException e) {
|
} catch (IOException | ID3ReaderException e) {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
import org.apache.commons.io.input.CountingInputStream;
|
import org.apache.commons.io.input.CountingInputStream;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -120,17 +121,17 @@ public class ChapterUtils {
|
||||||
if (!source.exists()) {
|
if (!source.exists()) {
|
||||||
throw new IOException("Local file does not exist");
|
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)) {
|
} else if (playable.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) {
|
||||||
Uri uri = Uri.parse(playable.getStreamUrl());
|
Uri uri = Uri.parse(playable.getStreamUrl());
|
||||||
return new CountingInputStream(context.getContentResolver().openInputStream(uri));
|
return new CountingInputStream(new BufferedInputStream(context.getContentResolver().openInputStream(uri)));
|
||||||
} else {
|
} else {
|
||||||
Request request = new Request.Builder().url(playable.getStreamUrl()).build();
|
Request request = new Request.Builder().url(playable.getStreamUrl()).build();
|
||||||
Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
|
Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
|
||||||
if (response.body() == null) {
|
if (response.body() == null) {
|
||||||
throw new IOException("Body is 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
|
@NonNull
|
||||||
private static List<Chapter> readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException {
|
private static List<Chapter> readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException {
|
||||||
VorbisCommentChapterReader reader = new VorbisCommentChapterReader(input);
|
VorbisCommentChapterReader reader = new VorbisCommentChapterReader(new BufferedInputStream(input));
|
||||||
reader.readInputStream();
|
reader.readInputStream();
|
||||||
List<Chapter> chapters = reader.getChapters();
|
List<Chapter> chapters = reader.getChapters();
|
||||||
if (chapters == null) {
|
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 android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.documentfile.provider.AssetsDocumentFile;
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry;
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
|
||||||
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
|
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.Feed;
|
||||||
import de.danoeh.antennapod.model.feed.FeedItem;
|
import de.danoeh.antennapod.model.feed.FeedItem;
|
||||||
import de.danoeh.antennapod.storage.database.PodDBAdapter;
|
import de.danoeh.antennapod.storage.database.PodDBAdapter;
|
||||||
|
@ -24,11 +23,12 @@ import org.mockito.Mockito;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
import org.robolectric.shadows.ShadowMediaMetadataRetriever;
|
import org.robolectric.shadows.ShadowMediaMetadataRetriever;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.File;
|
||||||
import java.util.Calendar;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Arrays;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import de.danoeh.antennapod.core.ApplicationCallbacks;
|
import de.danoeh.antennapod.core.ApplicationCallbacks;
|
||||||
import de.danoeh.antennapod.core.ClientConfig;
|
import de.danoeh.antennapod.core.ClientConfig;
|
||||||
|
@ -60,8 +60,8 @@ public class LocalFeedUpdaterTest {
|
||||||
*/
|
*/
|
||||||
private static final String FEED_URL =
|
private static final String FEED_URL =
|
||||||
"content://com.android.externalstorage.documents/tree/primary%3ADownload%2Flocal-feed";
|
"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_DIR1 = "src/test/assets/local-feed1";
|
||||||
private static final String LOCAL_FEED_DIR2 = "local-feed2";
|
private static final String LOCAL_FEED_DIR2 = "src/test/assets/local-feed2";
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
|
@ -172,74 +172,61 @@ public class LocalFeedUpdaterTest {
|
||||||
|
|
||||||
Feed feed = verifySingleFeedInDatabase();
|
Feed feed = verifySingleFeedInDatabase();
|
||||||
List<FeedItem> feedItems = DBReader.getFeedItemList(feed);
|
List<FeedItem> feedItems = DBReader.getFeedItemList(feed);
|
||||||
FeedItem feedItem = feedItems.get(0);
|
assertEquals("track1.mp3", feedItems.get(0).getTitle());
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetImageUrl_EmptyFolder() {
|
public void testGetImageUrl_EmptyFolder() {
|
||||||
DocumentFile documentFolder = mockDocumentFolder();
|
String imageUrl = LocalFeedUpdater.getImageUrl(Collections.emptyList(), Uri.EMPTY);
|
||||||
String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
|
|
||||||
assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
|
assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetImageUrl_NoImageButAudioFiles() {
|
public void testGetImageUrl_NoImageButAudioFiles() {
|
||||||
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"));
|
List<FastDocumentFile> folder = Collections.singletonList(mockDocumentFile("audio.mp3", "audio/mp3"));
|
||||||
String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
|
String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY);
|
||||||
assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
|
assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetImageUrl_PreferredImagesFilenames() {
|
public void testGetImageUrl_PreferredImagesFilenames() {
|
||||||
for (String filename : LocalFeedUpdater.PREFERRED_FEED_IMAGE_FILENAMES) {
|
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
|
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));
|
assertThat(imageUrl, endsWith(filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetImageUrl_OtherImageFilenameJpg() {
|
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"));
|
mockDocumentFile("my-image.jpg", "image/jpeg"));
|
||||||
String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
|
String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY);
|
||||||
assertThat(imageUrl, endsWith("my-image.jpg"));
|
assertThat(imageUrl, endsWith("my-image.jpg"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetImageUrl_OtherImageFilenameJpeg() {
|
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"));
|
mockDocumentFile("my-image.jpeg", "image/jpeg"));
|
||||||
String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
|
String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY);
|
||||||
assertThat(imageUrl, endsWith("my-image.jpeg"));
|
assertThat(imageUrl, endsWith("my-image.jpeg"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetImageUrl_OtherImageFilenamePng() {
|
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"));
|
mockDocumentFile("my-image.png", "image/png"));
|
||||||
String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
|
String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY);
|
||||||
assertThat(imageUrl, endsWith("my-image.png"));
|
assertThat(imageUrl, endsWith("my-image.png"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetImageUrl_OtherImageFilenameUnsupportedMimeType() {
|
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"));
|
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));
|
assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,9 +235,8 @@ public class LocalFeedUpdaterTest {
|
||||||
*
|
*
|
||||||
* @param localFeedDir assets local feed folder with media files
|
* @param localFeedDir assets local feed folder with media files
|
||||||
*/
|
*/
|
||||||
private void mapDummyMetadata(@NonNull String localFeedDir) throws IOException {
|
private void mapDummyMetadata(@NonNull String localFeedDir) {
|
||||||
String[] fileNames = context.getAssets().list(localFeedDir);
|
for (String fileName : Objects.requireNonNull(new File(localFeedDir).list())) {
|
||||||
for (String fileName : fileNames) {
|
|
||||||
String path = localFeedDir + '/' + fileName;
|
String path = localFeedDir + '/' + fileName;
|
||||||
ShadowMediaMetadataRetriever.addMetadata(path,
|
ShadowMediaMetadataRetriever.addMetadata(path,
|
||||||
MediaMetadataRetriever.METADATA_KEY_DURATION, "10");
|
MediaMetadataRetriever.METADATA_KEY_DURATION, "10");
|
||||||
|
@ -259,24 +245,21 @@ public class LocalFeedUpdaterTest {
|
||||||
ShadowMediaMetadataRetriever.addMetadata(path,
|
ShadowMediaMetadataRetriever.addMetadata(path,
|
||||||
MediaMetadataRetriever.METADATA_KEY_DATE, "20200601T222324");
|
MediaMetadataRetriever.METADATA_KEY_DATE, "20200601T222324");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the method {@link LocalFeedUpdater#updateFeed(Feed, Context)} with
|
* Calls the method LocalFeedUpdater#tryUpdateFeed with the given local feed folder.
|
||||||
* the given local feed folder.
|
|
||||||
*
|
*
|
||||||
* @param localFeedDir assets local feed folder with media files
|
* @param localFeedDir assets local feed folder with media files
|
||||||
*/
|
*/
|
||||||
private void callUpdateFeed(@NonNull String localFeedDir) {
|
private void callUpdateFeed(@NonNull String localFeedDir) {
|
||||||
DocumentFile documentFile = new AssetsDocumentFile(localFeedDir, context.getAssets());
|
try (MockedStatic<FastDocumentFile> dfMock = Mockito.mockStatic(FastDocumentFile.class)) {
|
||||||
try (MockedStatic<DocumentFile> dfMock = Mockito.mockStatic(DocumentFile.class)) {
|
|
||||||
// mock external storage
|
// mock external storage
|
||||||
dfMock.when(() -> DocumentFile.fromTreeUri(any(), any())).thenReturn(documentFile);
|
dfMock.when(() -> FastDocumentFile.list(any(), any())).thenReturn(mockLocalFolder(localFeedDir));
|
||||||
|
|
||||||
// call method to test
|
// call method to test
|
||||||
Feed feed = new Feed(FEED_URL, null);
|
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.
|
* Create a DocumentFile mock object.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
private static DocumentFile mockDocumentFile(@NonNull String fileName, @NonNull String mimeType) {
|
private static FastDocumentFile mockDocumentFile(@NonNull String fileName, @NonNull String mimeType) {
|
||||||
DocumentFile file = mock(DocumentFile.class);
|
return new FastDocumentFile(fileName, mimeType, Uri.parse("file:///path/" + fileName), 0, 0);
|
||||||
when(file.getName()).thenReturn(fileName);
|
|
||||||
when(file.getUri()).thenReturn(Uri.parse("file:///path/" + fileName));
|
|
||||||
when(file.getType()).thenReturn(mimeType);
|
|
||||||
return file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static List<FastDocumentFile> mockLocalFolder(String folderName) {
|
||||||
* Create a DocumentFile folder mock object with a list of files.
|
List<FastDocumentFile> files = new ArrayList<>();
|
||||||
*/
|
for (File f : Objects.requireNonNull(new File(folderName).listFiles())) {
|
||||||
@NonNull
|
String extension = MimeTypeMap.getFileExtensionFromUrl(f.getPath());
|
||||||
private static DocumentFile mockDocumentFolder(DocumentFile... files) {
|
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
DocumentFile documentFolder = mock(DocumentFile.class);
|
files.add(new FastDocumentFile(f.getName(), mimeType,
|
||||||
when(documentFolder.listFiles()).thenReturn(files);
|
Uri.parse(f.toURI().toString()), f.length(), f.lastModified()));
|
||||||
return documentFolder;
|
}
|
||||||
|
return files;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ public class ID3Reader {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void readFrame(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException {
|
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());
|
skipBytes(frameHeader.getSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ public class ID3Reader {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
FrameHeader readFrameHeader() throws IOException {
|
FrameHeader readFrameHeader() throws IOException {
|
||||||
String id = readIsoStringFixed(FRAME_ID_LENGTH);
|
String id = readPlainBytesToString(FRAME_ID_LENGTH);
|
||||||
int size = readInt();
|
int size = readInt();
|
||||||
if (tagHeader != null && tagHeader.getVersion() >= 0x0400) {
|
if (tagHeader != null && tagHeader.getVersion() >= 0x0400) {
|
||||||
size = unsynchsafe(size);
|
size = unsynchsafe(size);
|
||||||
|
@ -136,15 +136,14 @@ public class ID3Reader {
|
||||||
return readEncodedString(encoding, max - 1);
|
return readEncodedString(encoding, max - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("CharsetObjectCanBeUsed")
|
protected String readPlainBytesToString(int length) throws IOException {
|
||||||
protected String readIsoStringFixed(int length) throws IOException {
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
|
||||||
int bytesRead = 0;
|
int bytesRead = 0;
|
||||||
while (bytesRead < length) {
|
while (bytesRead < length) {
|
||||||
bytes.write(readByte());
|
stringBuilder.append((char) readByte());
|
||||||
bytesRead++;
|
bytesRead++;
|
||||||
}
|
}
|
||||||
return Charset.forName("ISO-8859-1").newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString();
|
return stringBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String readIsoStringNullTerminated(int max) throws IOException {
|
protected String readIsoStringNullTerminated(int max) throws IOException {
|
||||||
|
|
|
@ -1,18 +1,7 @@
|
||||||
package de.danoeh.antennapod.parser.media.id3.model;
|
package de.danoeh.antennapod.parser.media.id3.model;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class FrameHeader extends Header {
|
public class FrameHeader extends Header {
|
||||||
private final short flags;
|
|
||||||
|
|
||||||
public FrameHeader(String id, int size, short flags) {
|
public FrameHeader(String id, int size, short flags) {
|
||||||
super(id, size);
|
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