From 2fd73b148d012fba7308c86494689103b8aaace4 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Fri, 29 Mar 2024 19:27:53 +0100 Subject: Move download service to module (#7041) --- core/build.gradle | 6 - .../danoeh/antennapod/core/ClientConfigurator.java | 62 ---- .../antennapod/core/backup/OpmlBackupAgent.java | 188 ------------ .../antennapod/core/feed/LocalFeedUpdater.java | 286 ------------------- .../core/receiver/FeedUpdateReceiver.java | 3 - .../core/receiver/MediaButtonReceiver.java | 3 - .../antennapod/core/service/FeedUpdateWorker.java | 221 --------------- .../service/download/DefaultDownloaderFactory.java | 21 -- .../service/download/DownloadRequestCreator.java | 123 -------- .../download/DownloadServiceInterfaceImpl.java | 96 ------- .../core/service/download/Downloader.java | 62 ---- .../core/service/download/DownloaderFactory.java | 10 - .../service/download/EpisodeDownloadWorker.java | 311 -------------------- .../core/service/download/HttpDownloader.java | 314 --------------------- .../service/download/NewEpisodesNotification.java | 147 ---------- .../service/download/handler/FeedParserTask.java | 125 -------- .../download/handler/MediaDownloadedHandler.java | 119 -------- .../antennapod/core/util/FastDocumentFile.java | 72 ----- .../antennapod/core/util/FileNameGenerator.java | 76 ----- .../antennapod/core/util/InvalidFeedException.java | 12 - .../core/util/download/FeedUpdateManagerImpl.java | 118 -------- core/src/main/res/values/ids.xml | 4 - core/src/test/assets/local-feed1/track1.mp3 | Bin 43341 -> 0 bytes core/src/test/assets/local-feed2/folder.png | Bin 1589 -> 0 bytes core/src/test/assets/local-feed2/track1.mp3 | Bin 43341 -> 0 bytes core/src/test/assets/local-feed2/track2.mp3 | Bin 43497 -> 0 bytes .../antennapod/core/feed/LocalFeedUpdaterTest.java | 307 -------------------- .../core/util/FilenameGeneratorTest.java | 1 + 28 files changed, 1 insertion(+), 2686 deletions(-) delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/ClientConfigurator.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DefaultDownloaderFactory.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderFactory.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/FastDocumentFile.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManagerImpl.java delete mode 100644 core/src/test/assets/local-feed1/track1.mp3 delete mode 100644 core/src/test/assets/local-feed2/folder.png delete mode 100644 core/src/test/assets/local-feed2/track1.mp3 delete mode 100644 core/src/test/assets/local-feed2/track2.mp3 delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java (limited to 'core') diff --git a/core/build.gradle b/core/build.gradle index b2fffa68b..f7d736968 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -27,22 +27,16 @@ dependencies { implementation project(':model') implementation project(':net:common') implementation project(':net:download:service-interface') - implementation project(':net:ssl') - implementation project(':net:sync:gpoddernet') - implementation project(':net:sync:model') - implementation project(':net:sync:service') implementation project(':net:sync:service-interface') implementation project(':parser:feed') implementation project(':parser:media') implementation project(':playback:base') implementation project(':playback:cast') implementation project(':storage:database') - implementation project(':storage:importexport') implementation project(':storage:preferences') implementation project(':ui:app-start-intent') implementation project(':ui:common') implementation project(':ui:episodes') - implementation project(':ui:i18n') implementation project(':ui:notifications') implementation project(':ui:widget') diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfigurator.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfigurator.java deleted file mode 100644 index 8b5f9f286..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfigurator.java +++ /dev/null @@ -1,62 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import de.danoeh.antennapod.core.storage.AutoDownloadManagerImpl; -import de.danoeh.antennapod.core.util.download.FeedUpdateManagerImpl; -import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.net.sync.service.SyncService; -import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; -import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; -import de.danoeh.antennapod.storage.preferences.SynchronizationCredentials; -import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; -import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences; -import de.danoeh.antennapod.storage.preferences.UsageStatistics; -import de.danoeh.antennapod.net.common.UserAgentInterceptor; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.net.common.AntennapodHttpClient; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.core.service.download.DownloadServiceInterfaceImpl; -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.core.util.download.NetworkConnectionChangeHandler; -import de.danoeh.antennapod.net.ssl.SslProviderInstaller; -import de.danoeh.antennapod.storage.database.PodDBAdapter; - -import de.danoeh.antennapod.ui.notifications.NotificationUtils; -import java.io.File; - -public class ClientConfigurator { - private static boolean initialized = false; - - public static synchronized void initialize(Context context) { - if (initialized) { - return; - } - try { - PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); - UserAgentInterceptor.USER_AGENT = "AntennaPod/" + packageInfo.versionName; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - PodDBAdapter.init(context); - UserPreferences.init(context); - SynchronizationCredentials.init(context); - SynchronizationSettings.init(context); - UsageStatistics.init(context); - PlaybackPreferences.init(context); - SslProviderInstaller.install(context); - NetworkUtils.init(context); - NetworkConnectionChangeHandler.init(context); - DownloadServiceInterface.setImpl(new DownloadServiceInterfaceImpl()); - FeedUpdateManager.setInstance(new FeedUpdateManagerImpl()); - AutoDownloadManager.setInstance(new AutoDownloadManagerImpl()); - SynchronizationQueueSink.setServiceStarterImpl(() -> SyncService.sync(context)); - AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp")); - AntennapodHttpClient.setProxyConfig(UserPreferences.getProxyConfig()); - SleepTimerPreferences.init(context); - NotificationUtils.createChannels(context); - initialized = true; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java deleted file mode 100644 index b30f657a1..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java +++ /dev/null @@ -1,188 +0,0 @@ -package de.danoeh.antennapod.core.backup; - -import android.app.backup.BackupAgentHelper; -import android.app.backup.BackupDataInputStream; -import android.app.backup.BackupDataOutput; -import android.app.backup.BackupHelper; -import android.content.Context; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; -import de.danoeh.antennapod.storage.importexport.OpmlElement; -import de.danoeh.antennapod.storage.importexport.OpmlReader; -import de.danoeh.antennapod.storage.importexport.OpmlWriter; -import org.apache.commons.io.IOUtils; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; -import java.math.BigInteger; -import java.nio.charset.Charset; -import java.security.DigestInputStream; -import java.security.DigestOutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.storage.database.DBReader; - -public class OpmlBackupAgent extends BackupAgentHelper { - private static final String OPML_BACKUP_KEY = "opml"; - - @Override - public void onCreate() { - addHelper(OPML_BACKUP_KEY, new OpmlBackupHelper(this)); - } - - /** - * Class for backing up and restoring the OPML file. - */ - private static class OpmlBackupHelper implements BackupHelper { - private static final String TAG = "OpmlBackupHelper"; - - private static final String OPML_ENTITY_KEY = "antennapod-feeds.opml"; - - private final Context mContext; - - /** - * Checksum of restored OPML file - */ - private byte[] mChecksum; - - public OpmlBackupHelper(Context context) { - mContext = context; - } - - @Override - public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { - Log.d(TAG, "Performing backup"); - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - MessageDigest digester = null; - Writer writer; - - try { - digester = MessageDigest.getInstance("MD5"); - writer = new OutputStreamWriter(new DigestOutputStream(byteStream, digester), - Charset.forName("UTF-8")); - } catch (NoSuchAlgorithmException e) { - writer = new OutputStreamWriter(byteStream, Charset.forName("UTF-8")); - } - - try { - // Write OPML - OpmlWriter.writeDocument(DBReader.getFeedList(), writer); - - // Compare checksum of new and old file to see if we need to perform a backup at all - if (digester != null) { - byte[] newChecksum = digester.digest(); - Log.d(TAG, "New checksum: " + new BigInteger(1, newChecksum).toString(16)); - - // Get the old checksum - if (oldState != null) { - try (final FileInputStream inState = new FileInputStream(oldState.getFileDescriptor())) { - int len = inState.read(); - - if (len != -1) { - byte[] oldChecksum = new byte[len]; - IOUtils.read(inState, oldChecksum, 0, len); - Log.d(TAG, "Old checksum: " + new BigInteger(1, oldChecksum).toString(16)); - - if (Arrays.equals(oldChecksum, newChecksum)) { - Log.d(TAG, "Checksums are the same; won't backup"); - return; - } - } - } - } - - writeNewStateDescription(newState, newChecksum); - } - - Log.d(TAG, "Backing up OPML"); - byte[] bytes = byteStream.toByteArray(); - data.writeEntityHeader(OPML_ENTITY_KEY, bytes.length); - data.writeEntityData(bytes, bytes.length); - } catch (IOException e) { - Log.e(TAG, "Error during backup", e); - } finally { - IOUtils.closeQuietly(writer); - } - } - - @Override - public void restoreEntity(BackupDataInputStream data) { - Log.d(TAG, "Backup restore"); - - if (!OPML_ENTITY_KEY.equals(data.getKey())) { - Log.d(TAG, "Unknown entity key: " + data.getKey()); - return; - } - - MessageDigest digester = null; - Reader reader; - - try { - digester = MessageDigest.getInstance("MD5"); - reader = new InputStreamReader(new DigestInputStream(data, digester), - Charset.forName("UTF-8")); - } catch (NoSuchAlgorithmException e) { - reader = new InputStreamReader(data, Charset.forName("UTF-8")); - } - - try { - ArrayList opmlElements = new OpmlReader().readDocument(reader); - mChecksum = digester == null ? null : digester.digest(); - for (OpmlElement opmlElem : opmlElements) { - Feed feed = new Feed(opmlElem.getXmlUrl(), null, opmlElem.getText()); - feed.setItems(Collections.emptyList()); - FeedDatabaseWriter.updateFeed(mContext, feed, false); - } - FeedUpdateManager.getInstance().runOnce(mContext); - } catch (XmlPullParserException e) { - Log.e(TAG, "Error while parsing the OPML file", e); - } catch (IOException e) { - Log.e(TAG, "Failed to restore OPML backup", e); - } finally { - IOUtils.closeQuietly(reader); - } - } - - @Override - public void writeNewStateDescription(ParcelFileDescriptor newState) { - writeNewStateDescription(newState, mChecksum); - } - - /** - * Writes the new state description, which is the checksum of the OPML file. - * - * @param newState - * @param checksum - */ - private void writeNewStateDescription(ParcelFileDescriptor newState, byte[] checksum) { - if (checksum == null) { - return; - } - - try { - FileOutputStream outState = new FileOutputStream(newState.getFileDescriptor()); - outState.write(checksum.length); - outState.write(checksum); - outState.flush(); - outState.close(); - } catch (IOException e) { - Log.e(TAG, "Failed to write new state description", e); - } - } - } -} 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 deleted file mode 100644 index 8230924f9..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ /dev/null @@ -1,286 +0,0 @@ -package de.danoeh.antennapod.core.feed; - -import android.content.Context; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.text.TextUtils; -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; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -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.MediaMetadataRetrieverCompat; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.parser.feed.util.DateUtils; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.parser.feed.util.MimeTypeUtils; -import de.danoeh.antennapod.parser.media.id3.ID3ReaderException; -import de.danoeh.antennapod.parser.media.id3.Id3MetadataReader; -import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentMetadataReader; -import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException; -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"}; - - public static void updateFeed(Feed feed, Context context, - @Nullable UpdaterProgressListener updaterProgressListener) { - try { - String uriString = feed.getDownloadUrl().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); - } - } catch (Exception e) { - e.printStackTrace(); - reportError(feed, e.getMessage()); - } - } - - @VisibleForTesting - static void tryUpdateFeed(Feed feed, Context context, Uri folderUri, - UpdaterProgressListener updaterProgressListener) throws IOException { - if (feed.getItems() == null) { - feed.setItems(new ArrayList<>()); - } - //make sure it is the latest 'version' of this feed from the db (all items etc) - feed = FeedDatabaseWriter.updateFeed(context, feed, false); - - // list files in feed folder - List allFiles = FastDocumentFile.list(context, folderUri); - List mediaFiles = new ArrayList<>(); - Set mediaFileNames = new HashSet<>(); - 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) { - mediaFiles.add(file); - mediaFileNames.add(file.getName()); - } - } - - // add new files to feed and update item data - List newItems = feed.getItems(); - for (int i = 0; i < mediaFiles.size(); i++) { - FeedItem oldItem = feedContainsFile(feed, mediaFiles.get(i).getName()); - FeedItem newItem = createFeedItem(feed, mediaFiles.get(i), context); - if (oldItem == null) { - newItems.add(newItem); - } else { - oldItem.updateFromOther(newItem); - } - if (updaterProgressListener != null) { - updaterProgressListener.onLocalFileScanned(i, mediaFiles.size()); - } - } - - // remove feed items without corresponding file - Iterator it = newItems.iterator(); - while (it.hasNext()) { - FeedItem feedItem = it.next(); - if (!mediaFileNames.contains(feedItem.getLink())) { - it.remove(); - } - } - - feed.setImageUrl(getImageUrl(allFiles, folderUri)); - - feed.getPreferences().setAutoDownload(false); - feed.setDescription(context.getString(R.string.local_feed_description)); - feed.setAuthor(context.getString(R.string.local_folder)); - - FeedDatabaseWriter.updateFeed(context, feed, true); - } - - /** - * Returns the image URL for the local feed. - */ - @NonNull - static String getImageUrl(List files, Uri folderUri) { - // look for special file names - for (String iconLocation : PREFERRED_FEED_IMAGE_FILENAMES) { - for (FastDocumentFile file : files) { - if (iconLocation.equals(file.getName())) { - return file.getUri().toString(); - } - } - } - - // use the first image in the folder if existing - for (FastDocumentFile file : files) { - String mime = file.getType(); - if (mime != null && (mime.startsWith("image/jpeg") || mime.startsWith("image/png"))) { - return file.getUri().toString(); - } - } - - // use default icon as fallback - return Feed.PREFIX_GENERATIVE_COVER + folderUri; - } - - private static FeedItem feedContainsFile(Feed feed, String filename) { - List items = feed.getItems(); - for (FeedItem i : items) { - if (i.getMedia() != null && i.getLink().equals(filename)) { - return i; - } - } - return null; - } - - 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.getLastModified()), FeedItem.UNPLAYED, feed); - item.disableAutoDownload(); - - 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().getDownloadUrl().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, FastDocumentFile file, Context context) { - try (MediaMetadataRetrieverCompat mediaMetadataRetriever = new MediaMetadataRetrieverCompat()) { - mediaMetadataRetriever.setDataSource(context, file.getUri()); - - String dateStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE); - if (!TextUtils.isEmpty(dateStr) && !"19040101T000000.000Z".equals(dateStr)) { - try { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault()); - item.setPubDate(simpleDateFormat.parse(dateStr)); - } catch (ParseException parseException) { - Date date = DateUtils.parse(dateStr); - if (date != null) { - item.setPubDate(date); - } - } - } - - String title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); - if (!TextUtils.isEmpty(title)) { - item.setTitle(title); - } - - String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - item.getMedia().setDuration((int) Long.parseLong(durationStr)); - - item.getMedia().setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null); - - try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) { - Id3MetadataReader reader = new Id3MetadataReader( - new CountingInputStream(new BufferedInputStream(inputStream))); - reader.readInputStream(); - item.setDescriptionIfLonger(reader.getComment()); - } catch (IOException | ID3ReaderException e) { - Log.d(TAG, "Unable to parse ID3 of " + file.getUri() + ": " + e.getMessage()); - - try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) { - VorbisCommentMetadataReader reader = new VorbisCommentMetadataReader(inputStream); - reader.readInputStream(); - item.setDescriptionIfLonger(reader.getDescription()); - } catch (IOException | VorbisCommentReaderException e2) { - Log.d(TAG, "Unable to parse vorbis comments of " + file.getUri() + ": " + e2.getMessage()); - } - } - } - } - - private static void reportError(Feed feed, String reasonDetailed) { - DownloadResult status = new DownloadResult(feed.getTitle(), feed.getId(), - Feed.FEEDFILETYPE_FEED, false, DownloadError.ERROR_IO_ERROR, reasonDetailed); - DBWriter.addDownloadStatus(status); - DBWriter.setFeedLastUpdateFailed(feed.getId(), true); - } - - /** - * Reports a successful download status. - */ - private static void reportSuccess(Feed feed) { - DownloadResult status = new DownloadResult(feed.getTitle(), feed.getId(), - Feed.FEEDFILETYPE_FEED, true, DownloadError.SUCCESS, null); - DBWriter.addDownloadStatus(status); - DBWriter.setFeedLastUpdateFailed(feed.getId(), false); - } - - /** - * Answers if reporting success is needed for the given feed. - */ - private static boolean mustReportDownloadSuccessful(Feed feed) { - List downloadResults = DBReader.getFeedDownloadLog(feed.getId()); - - if (downloadResults.isEmpty()) { - // report success if never reported before - return true; - } - - Collections.sort(downloadResults, (downloadStatus1, downloadStatus2) -> - downloadStatus1.getCompletionDate().compareTo(downloadStatus2.getCompletionDate())); - - DownloadResult lastDownloadResult = downloadResults.get(downloadResults.size() - 1); - - // report success if the last update was not successful - // (avoid logging success again if the last update was ok) - return !lastDownloadResult.isSuccessful(); - } - - @FunctionalInterface - public interface UpdaterProgressListener { - void onLocalFileScanned(int scanned, int totalFiles); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java index 098c9bfa4..6b9644c41 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java @@ -5,7 +5,6 @@ import android.content.Context; import android.content.Intent; import android.util.Log; -import de.danoeh.antennapod.core.ClientConfigurator; import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; /** @@ -18,8 +17,6 @@ public class FeedUpdateReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "Received intent"); - ClientConfigurator.initialize(context); - FeedUpdateManager.getInstance().runOnce(context); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java index 8721ebb35..20621fd45 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java @@ -7,8 +7,6 @@ import androidx.core.content.ContextCompat; import android.util.Log; import android.view.KeyEvent; -import de.danoeh.antennapod.core.ClientConfigurator; - /** * Receives media button events. */ @@ -30,7 +28,6 @@ public class MediaButtonReceiver extends BroadcastReceiver { } KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); if (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { - ClientConfigurator.initialize(context); Intent serviceIntent = new Intent(PLAYBACK_SERVICE_INTENT); serviceIntent.setPackage(context.getPackageName()); serviceIntent.putExtra(EXTRA_KEYCODE, event.getKeyCode()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java deleted file mode 100644 index e5828ac6e..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java +++ /dev/null @@ -1,221 +0,0 @@ -package de.danoeh.antennapod.core.service; - -import android.Manifest; -import android.app.Notification; -import android.content.Context; -import android.content.pm.PackageManager; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.ContextCompat; -import androidx.work.ForegroundInfo; -import androidx.work.WorkManager; -import androidx.work.Worker; -import androidx.work.WorkerParameters; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import de.danoeh.antennapod.core.ClientConfigurator; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.feed.LocalFeedUpdater; -import de.danoeh.antennapod.core.service.download.DefaultDownloaderFactory; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.core.service.download.Downloader; -import de.danoeh.antennapod.core.service.download.NewEpisodesNotification; -import de.danoeh.antennapod.core.service.download.handler.FeedParserTask; -import de.danoeh.antennapod.core.util.download.FeedUpdateManagerImpl; -import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.download.DownloadRequest; - -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequestBuilder; -import de.danoeh.antennapod.parser.feed.FeedHandlerResult; -import de.danoeh.antennapod.ui.notifications.NotificationUtils; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -public class FeedUpdateWorker extends Worker { - private static final String TAG = "FeedUpdateWorker"; - - private final NewEpisodesNotification newEpisodesNotification; - private final NotificationManagerCompat notificationManager; - - public FeedUpdateWorker(@NonNull Context context, @NonNull WorkerParameters params) { - super(context, params); - newEpisodesNotification = new NewEpisodesNotification(); - notificationManager = NotificationManagerCompat.from(context); - } - - @Override - @NonNull - public Result doWork() { - ClientConfigurator.initialize(getApplicationContext()); - newEpisodesNotification.loadCountersBeforeRefresh(); - - List toUpdate; - long feedId = getInputData().getLong(FeedUpdateManagerImpl.EXTRA_FEED_ID, -1); - boolean allAreLocal = true; - boolean force = false; - if (feedId == -1) { // Update all - toUpdate = DBReader.getFeedList(); - Iterator itr = toUpdate.iterator(); - while (itr.hasNext()) { - Feed feed = itr.next(); - if (!feed.getPreferences().getKeepUpdated()) { - itr.remove(); - } - if (!feed.isLocalFeed()) { - allAreLocal = false; - } - } - Collections.shuffle(toUpdate); // If the worker gets cancelled early, every feed has a chance to be updated - } else { - Feed feed = DBReader.getFeed(feedId); - if (feed == null) { - return Result.success(); - } - if (!feed.isLocalFeed()) { - allAreLocal = false; - } - toUpdate = new ArrayList<>(); - toUpdate.add(feed); // Needs to be updatable, so no singletonList - force = true; - } - - if (!getInputData().getBoolean(FeedUpdateManagerImpl.EXTRA_EVEN_ON_MOBILE, false) && !allAreLocal) { - if (!NetworkUtils.networkAvailable() || !NetworkUtils.isFeedRefreshAllowed()) { - Log.d(TAG, "Blocking automatic update"); - return Result.retry(); - } - } - refreshFeeds(toUpdate, force); - - notificationManager.cancel(R.id.notification_updating_feeds); - AutoDownloadManager.getInstance().autodownloadUndownloadedItems(getApplicationContext()); - return Result.success(); - } - - @NonNull - private Notification createNotification(@Nullable List toUpdate) { - Context context = getApplicationContext(); - String contentText = ""; - StringBuilder bigText = new StringBuilder(); - if (toUpdate != null) { - contentText = context.getResources().getQuantityString(R.plurals.downloads_left, - toUpdate.size(), toUpdate.size()); - for (int i = 0; i < toUpdate.size(); i++) { - bigText.append("• ").append(toUpdate.get(i).getTitle()); - if (i != toUpdate.size() - 1) { - bigText.append("\n"); - } - } - } - return new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_DOWNLOADING) - .setContentTitle(context.getString(R.string.download_notification_title_feeds)) - .setContentText(contentText) - .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) - .setSmallIcon(R.drawable.ic_notification_sync) - .setOngoing(true) - .addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label), - WorkManager.getInstance(context).createCancelPendingIntent(getId())) - .build(); - } - - @NonNull - @Override - public ListenableFuture getForegroundInfoAsync() { - return Futures.immediateFuture(new ForegroundInfo(R.id.notification_updating_feeds, createNotification(null))); - } - - private void refreshFeeds(List toUpdate, boolean force) { - while (!toUpdate.isEmpty()) { - if (isStopped()) { - return; - } - if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) - == PackageManager.PERMISSION_GRANTED) { - notificationManager.notify(R.id.notification_updating_feeds, createNotification(toUpdate)); - } - Feed feed = toUpdate.get(0); - try { - if (feed.isLocalFeed()) { - LocalFeedUpdater.updateFeed(feed, getApplicationContext(), null); - } else { - refreshFeed(feed, force); - } - } catch (Exception e) { - DBWriter.setFeedLastUpdateFailed(feed.getId(), true); - DownloadResult status = new DownloadResult(feed.getTitle(), - feed.getId(), Feed.FEEDFILETYPE_FEED, false, - DownloadError.ERROR_IO_ERROR, e.getMessage()); - DBWriter.addDownloadStatus(status); - } - toUpdate.remove(0); - } - } - - void refreshFeed(Feed feed, boolean force) throws Exception { - boolean nextPage = getInputData().getBoolean(FeedUpdateManagerImpl.EXTRA_NEXT_PAGE, false) - && feed.getNextPageLink() != null; - if (nextPage) { - feed.setPageNr(feed.getPageNr() + 1); - } - DownloadRequestBuilder builder = DownloadRequestCreator.create(feed); - builder.setForce(force || feed.hasLastUpdateFailed()); - if (nextPage) { - builder.setSource(feed.getNextPageLink()); - } - DownloadRequest request = builder.build(); - - Downloader downloader = new DefaultDownloaderFactory().create(request); - if (downloader == null) { - throw new Exception("Unable to create downloader"); - } - - downloader.call(); - - if (!downloader.getResult().isSuccessful()) { - if (downloader.cancelled || downloader.getResult().getReason() == DownloadError.ERROR_DOWNLOAD_CANCELLED) { - return; - } - DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); - DBWriter.addDownloadStatus(downloader.getResult()); - return; - } - - FeedParserTask parserTask = new FeedParserTask(request); - FeedHandlerResult feedHandlerResult = parserTask.call(); - if (!parserTask.isSuccessful()) { - DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); - DBWriter.addDownloadStatus(parserTask.getDownloadStatus()); - return; - } - feedHandlerResult.feed.setLastRefreshAttempt(System.currentTimeMillis()); - Feed savedFeed = FeedDatabaseWriter.updateFeed(getApplicationContext(), feedHandlerResult.feed, false); - - if (request.getFeedfileId() == 0) { - return; // No download logs for new subscriptions - } - // we create a 'successful' download log if the feed's last refresh failed - List log = DBReader.getFeedDownloadLog(request.getFeedfileId()); - if (!log.isEmpty() && !log.get(0).isSuccessful()) { - DBWriter.addDownloadStatus(parserTask.getDownloadStatus()); - } - newEpisodesNotification.showIfNeeded(getApplicationContext(), savedFeed); - if (downloader.permanentRedirectUrl != null) { - DBWriter.updateFeedDownloadURL(request.getSource(), downloader.permanentRedirectUrl); - } else if (feedHandlerResult.redirectUrl != null - && !feedHandlerResult.redirectUrl.equals(request.getSource())) { - DBWriter.updateFeedDownloadURL(request.getSource(), feedHandlerResult.redirectUrl); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DefaultDownloaderFactory.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DefaultDownloaderFactory.java deleted file mode 100644 index a1cc9bf6d..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DefaultDownloaderFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import android.util.Log; -import android.webkit.URLUtil; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import de.danoeh.antennapod.model.download.DownloadRequest; - -public class DefaultDownloaderFactory implements DownloaderFactory { - private static final String TAG = "DefaultDwnldrFactory"; - - @Nullable - @Override - public Downloader create(@NonNull DownloadRequest request) { - if (!URLUtil.isHttpUrl(request.getSource()) && !URLUtil.isHttpsUrl(request.getSource())) { - Log.e(TAG, "Could not find appropriate downloader for " + request.getSource()); - return null; - } - return new HttpDownloader(request); - } -} \ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java deleted file mode 100644 index 3e94e9f6b..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java +++ /dev/null @@ -1,123 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import android.util.Log; -import android.webkit.URLUtil; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequestBuilder; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.FileNameGenerator; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedMedia; -import org.apache.commons.io.FilenameUtils; - -import java.io.File; - -/** - * Creates download requests that can be sent to the DownloadService. - */ -public class DownloadRequestCreator { - private static final String TAG = "DownloadRequestCreat"; - private static final String FEED_DOWNLOADPATH = "cache/"; - private static final String MEDIA_DOWNLOADPATH = "media/"; - - public static DownloadRequestBuilder create(Feed feed) { - File dest = new File(getFeedfilePath(), getFeedfileName(feed)); - if (dest.exists()) { - dest.delete(); - } - Log.d(TAG, "Requesting download of url " + feed.getDownloadUrl()); - - String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null; - String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null; - - return new DownloadRequestBuilder(dest.toString(), feed) - .withAuthentication(username, password) - .lastModified(feed.getLastModified()); - } - - public static DownloadRequestBuilder create(FeedMedia media) { - final boolean partiallyDownloadedFileExists = - media.getLocalFileUrl() != null && new File(media.getLocalFileUrl()).exists(); - File dest; - if (partiallyDownloadedFileExists) { - dest = new File(media.getLocalFileUrl()); - } else { - dest = new File(getMediafilePath(media), getMediafilename(media)); - } - - if (dest.exists() && !partiallyDownloadedFileExists) { - dest = findUnusedFile(dest); - } - Log.d(TAG, "Requesting download of url " + media.getDownloadUrl()); - - String username = (media.getItem().getFeed().getPreferences() != null) - ? media.getItem().getFeed().getPreferences().getUsername() : null; - String password = (media.getItem().getFeed().getPreferences() != null) - ? media.getItem().getFeed().getPreferences().getPassword() : null; - - return new DownloadRequestBuilder(dest.toString(), media) - .withAuthentication(username, password); - } - - private static File findUnusedFile(File dest) { - // find different name - File newDest = null; - for (int i = 1; i < Integer.MAX_VALUE; i++) { - String newName = FilenameUtils.getBaseName(dest - .getName()) - + "-" - + i - + FilenameUtils.EXTENSION_SEPARATOR - + FilenameUtils.getExtension(dest.getName()); - Log.d(TAG, "Testing filename " + newName); - newDest = new File(dest.getParent(), newName); - if (!newDest.exists()) { - Log.d(TAG, "File doesn't exist yet. Using " + newName); - break; - } - } - return newDest; - } - - private static String getFeedfilePath() { - return UserPreferences.getDataFolder(FEED_DOWNLOADPATH).toString() + "/"; - } - - private static String getFeedfileName(Feed feed) { - String filename = feed.getDownloadUrl(); - if (feed.getTitle() != null && !feed.getTitle().isEmpty()) { - filename = feed.getTitle(); - } - return "feed-" + FileNameGenerator.generateFileName(filename) + feed.getId(); - } - - private static String getMediafilePath(FeedMedia media) { - String mediaPath = MEDIA_DOWNLOADPATH - + FileNameGenerator.generateFileName(media.getItem().getFeed().getTitle()); - return UserPreferences.getDataFolder(mediaPath).toString() + "/"; - } - - private static String getMediafilename(FeedMedia media) { - String titleBaseFilename = ""; - - // Try to generate the filename by the item title - if (media.getItem() != null && media.getItem().getTitle() != null) { - String title = media.getItem().getTitle(); - titleBaseFilename = FileNameGenerator.generateFileName(title); - } - - String urlBaseFilename = URLUtil.guessFileName(media.getDownloadUrl(), null, media.getMimeType()); - - String baseFilename; - if (!titleBaseFilename.equals("")) { - baseFilename = titleBaseFilename; - } else { - baseFilename = urlBaseFilename; - } - final int filenameMaxLength = 220; - if (baseFilename.length() > filenameMaxLength) { - baseFilename = baseFilename.substring(0, filenameMaxLength); - } - return baseFilename + FilenameUtils.EXTENSION_SEPARATOR + media.getId() - + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(urlBaseFilename); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java deleted file mode 100644 index e2489b493..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java +++ /dev/null @@ -1,96 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import android.content.Context; -import androidx.work.Constraints; -import androidx.work.Data; -import androidx.work.ExistingWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.OutOfQuotaPolicy; -import androidx.work.WorkInfo; -import androidx.work.WorkManager; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import io.reactivex.Observable; -import io.reactivex.schedulers.Schedulers; - -import java.util.List; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -public class DownloadServiceInterfaceImpl extends DownloadServiceInterface { - public void downloadNow(Context context, FeedItem item, boolean ignoreConstraints) { - OneTimeWorkRequest.Builder workRequest = getRequest(context, item); - workRequest.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST); - if (ignoreConstraints) { - workRequest.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()); - } else { - workRequest.setConstraints(getConstraints()); - } - WorkManager.getInstance(context).enqueueUniqueWork(item.getMedia().getDownloadUrl(), - ExistingWorkPolicy.KEEP, workRequest.build()); - } - - public void download(Context context, FeedItem item) { - OneTimeWorkRequest.Builder workRequest = getRequest(context, item); - workRequest.setConstraints(getConstraints()); - WorkManager.getInstance(context).enqueueUniqueWork(item.getMedia().getDownloadUrl(), - ExistingWorkPolicy.KEEP, workRequest.build()); - } - - private static OneTimeWorkRequest.Builder getRequest(Context context, FeedItem item) { - OneTimeWorkRequest.Builder workRequest = new OneTimeWorkRequest.Builder(EpisodeDownloadWorker.class) - .setInitialDelay(0L, TimeUnit.MILLISECONDS) - .addTag(DownloadServiceInterface.WORK_TAG) - .addTag(DownloadServiceInterface.WORK_TAG_EPISODE_URL + item.getMedia().getDownloadUrl()); - if (!item.isTagged(FeedItem.TAG_QUEUE) && UserPreferences.enqueueDownloadedEpisodes()) { - DBWriter.addQueueItem(context, false, item.getId()); - workRequest.addTag(DownloadServiceInterface.WORK_DATA_WAS_QUEUED); - } - workRequest.setInputData(new Data.Builder().putLong(WORK_DATA_MEDIA_ID, item.getMedia().getId()).build()); - return workRequest; - } - - private static Constraints getConstraints() { - Constraints.Builder constraints = new Constraints.Builder(); - if (UserPreferences.isAllowMobileEpisodeDownload()) { - constraints.setRequiredNetworkType(NetworkType.CONNECTED); - } else { - constraints.setRequiredNetworkType(NetworkType.UNMETERED); - } - return constraints.build(); - } - - @Override - public void cancel(Context context, FeedMedia media) { - // This needs to be done here, not in the worker. Reason: The worker might or might not be running. - if (media.fileExists()) { - DBWriter.deleteFeedMediaOfItem(context, media); // Remove partially downloaded file - } - String tag = WORK_TAG_EPISODE_URL + media.getDownloadUrl(); - Future> future = WorkManager.getInstance(context).getWorkInfosByTag(tag); - Observable.fromFuture(future) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe( - workInfos -> { - for (WorkInfo info : workInfos) { - if (info.getTags().contains(DownloadServiceInterface.WORK_DATA_WAS_QUEUED)) { - DBWriter.removeQueueItem(context, false, media.getItem()); - } - } - WorkManager.getInstance(context).cancelAllWorkByTag(tag); - }, exception -> { - WorkManager.getInstance(context).cancelAllWorkByTag(tag); - exception.printStackTrace(); - }); - } - - @Override - public void cancelAll(Context context) { - WorkManager.getInstance(context).cancelAllWorkByTag(WORK_TAG); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java deleted file mode 100644 index 7010d61ba..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java +++ /dev/null @@ -1,62 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import androidx.annotation.NonNull; - -import java.util.Date; -import java.util.concurrent.Callable; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.model.download.DownloadRequest; - -/** - * Downloads files - */ -public abstract class Downloader implements Callable { - private static final String TAG = "Downloader"; - - private volatile boolean finished; - public volatile boolean cancelled; - public String permanentRedirectUrl = null; - - @NonNull - final DownloadRequest request; - @NonNull - final DownloadResult result; - - Downloader(@NonNull DownloadRequest request) { - super(); - this.request = request; - this.request.setStatusMsg(R.string.download_pending); - this.cancelled = false; - this.result = new DownloadResult(0, request.getTitle(), request.getFeedfileId(), request.getFeedfileType(), - false, null, new Date(), null); - } - - protected abstract void download(); - - public final Downloader call() { - download(); - finished = true; - return this; - } - - @NonNull - public DownloadRequest getDownloadRequest() { - return request; - } - - @NonNull - public DownloadResult getResult() { - return result; - } - - public boolean isFinished() { - return finished; - } - - public void cancel() { - cancelled = true; - } - -} \ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderFactory.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderFactory.java deleted file mode 100644 index 45ad45381..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderFactory.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import de.danoeh.antennapod.model.download.DownloadRequest; - -public interface DownloaderFactory { - @Nullable - Downloader create(@NonNull DownloadRequest request); -} \ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java deleted file mode 100644 index a2b4ed100..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java +++ /dev/null @@ -1,311 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import android.Manifest; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.wifi.WifiManager; -import android.os.Build; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.core.content.ContextCompat; -import androidx.work.Data; -import androidx.work.ForegroundInfo; -import androidx.work.Worker; -import androidx.work.WorkerParameters; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import de.danoeh.antennapod.core.ClientConfigurator; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.download.DownloadRequest; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; -import de.danoeh.antennapod.ui.notifications.NotificationUtils; -import org.apache.commons.io.FileUtils; -import org.greenrobot.eventbus.EventBus; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.ExecutionException; - -public class EpisodeDownloadWorker extends Worker { - private static final String TAG = "EpisodeDownloadWorker"; - private static final Map notificationProgress = new HashMap<>(); - - private Downloader downloader = null; - - public EpisodeDownloadWorker(@NonNull Context context, @NonNull WorkerParameters params) { - super(context, params); - } - - @Override - @NonNull - public Result doWork() { - ClientConfigurator.initialize(getApplicationContext()); - long mediaId = getInputData().getLong(DownloadServiceInterface.WORK_DATA_MEDIA_ID, 0); - FeedMedia media = DBReader.getFeedMedia(mediaId); - if (media == null) { - return Result.failure(); - } - - DownloadRequest request = DownloadRequestCreator.create(media).build(); - Thread progressUpdaterThread = new Thread() { - @Override - public void run() { - while (true) { - try { - synchronized (notificationProgress) { - if (isInterrupted()) { - return; - } - notificationProgress.put(media.getEpisodeTitle(), request.getProgressPercent()); - } - setProgressAsync( - new Data.Builder() - .putInt(DownloadServiceInterface.WORK_DATA_PROGRESS, request.getProgressPercent()) - .build()) - .get(); - NotificationManager nm = (NotificationManager) getApplicationContext() - .getSystemService(Context.NOTIFICATION_SERVICE); - if (ContextCompat.checkSelfPermission(getApplicationContext(), - Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { - nm.notify(R.id.notification_downloading, generateProgressNotification()); - } - Thread.sleep(1000); - } catch (InterruptedException | ExecutionException e) { - return; - } - } - } - }; - progressUpdaterThread.start(); - Result result; - try { - result = performDownload(media, request); - } catch (Exception e) { - e.printStackTrace(); - result = Result.failure(); - } - if (result.equals(Result.failure()) && downloader != null) { - FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); - } - progressUpdaterThread.interrupt(); - try { - progressUpdaterThread.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - synchronized (notificationProgress) { - notificationProgress.remove(media.getEpisodeTitle()); - if (notificationProgress.isEmpty()) { - NotificationManager nm = (NotificationManager) getApplicationContext() - .getSystemService(Context.NOTIFICATION_SERVICE); - nm.cancel(R.id.notification_downloading); - } - } - Log.d(TAG, "Worker for " + media.getDownloadUrl() + " returned."); - return result; - } - - @Override - public void onStopped() { - super.onStopped(); - if (downloader != null) { - downloader.cancel(); - } - } - - @NonNull - @Override - public ListenableFuture getForegroundInfoAsync() { - return Futures.immediateFuture( - new ForegroundInfo(R.id.notification_downloading, generateProgressNotification())); - } - - private Result performDownload(FeedMedia media, DownloadRequest request) { - File dest = new File(request.getDestination()); - if (!dest.exists()) { - try { - dest.createNewFile(); - } catch (IOException e) { - Log.e(TAG, "Unable to create file"); - } - } - - if (dest.exists()) { - media.setLocalFileUrl(request.getDestination()); - try { - DBWriter.setFeedMedia(media).get(); - } catch (Exception e) { - Log.e(TAG, "ExecutionException in writeFileUrl: " + e.getMessage()); - } - } - - downloader = new DefaultDownloaderFactory().create(request); - if (downloader == null) { - Log.d(TAG, "Unable to create downloader"); - return Result.failure(); - } - - WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); - WifiManager.WifiLock wifiLock = null; - if (wifiManager != null) { - wifiLock = wifiManager.createWifiLock(TAG); - wifiLock.acquire(); - } - try { - downloader.call(); - } catch (Exception e) { - DBWriter.addDownloadStatus(downloader.getResult()); - sendErrorNotification(request.getTitle()); - return Result.failure(); - } finally { - if (wifiLock != null) { - wifiLock.release(); - } - } - - if (downloader.cancelled) { - // This also happens when the worker was preempted, not just when the user cancelled it - return Result.success(); - } - - DownloadResult status = downloader.getResult(); - if (status.isSuccessful()) { - MediaDownloadedHandler handler = new MediaDownloadedHandler( - getApplicationContext(), downloader.getResult(), request); - handler.run(); - DBWriter.addDownloadStatus(handler.getUpdatedStatus()); - return Result.success(); - } - - if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && Integer.parseInt(status.getReasonDetailed()) == 416) { - Log.d(TAG, "Requested invalid range, restarting download from the beginning"); - FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); - sendMessage(request.getTitle(), false); - return retry3times(); - } - - Log.e(TAG, "Download failed"); - DBWriter.addDownloadStatus(status); - if (status.getReason() == DownloadError.ERROR_FORBIDDEN - || status.getReason() == DownloadError.ERROR_NOT_FOUND - || status.getReason() == DownloadError.ERROR_UNAUTHORIZED - || status.getReason() == DownloadError.ERROR_IO_BLOCKED) { - // Fail fast, these are probably unrecoverable - sendErrorNotification(request.getTitle()); - return Result.failure(); - } - sendMessage(request.getTitle(), false); - return retry3times(); - } - - private Result retry3times() { - if (isLastRunAttempt()) { - sendErrorNotification(downloader.getDownloadRequest().getTitle()); - return Result.failure(); - } else { - return Result.retry(); - } - } - - private boolean isLastRunAttempt() { - return getRunAttemptCount() >= 2; - } - - private void sendMessage(String episodeTitle, boolean isImmediateFail) { - boolean retrying = !isLastRunAttempt() && !isImmediateFail; - if (episodeTitle.length() > 20) { - episodeTitle = episodeTitle.substring(0, 19) + "…"; - } - EventBus.getDefault().post(new MessageEvent( - getApplicationContext().getString( - retrying ? R.string.download_error_retrying : R.string.download_error_not_retrying, - episodeTitle), (ctx) -> new MainActivityStarter(ctx).withDownloadLogsOpen().start(), - getApplicationContext().getString(R.string.download_error_details))); - } - - private PendingIntent getDownloadLogsIntent(Context context) { - Intent intent = new MainActivityStarter(context).withDownloadLogsOpen().getIntent(); - return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent, - PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - private PendingIntent getDownloadsIntent(Context context) { - Intent intent = new MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent(); - return PendingIntent.getActivity(context, R.id.pending_intent_download_service_notification, intent, - PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - private void sendErrorNotification(String title) { - if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) { - sendMessage(title, false); - return; - } - - NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), - NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR); - builder.setTicker(getApplicationContext().getString(R.string.download_report_title)) - .setContentTitle(getApplicationContext().getString(R.string.download_report_title)) - .setContentText(getApplicationContext().getString(R.string.download_error_tap_for_details)) - .setSmallIcon(R.drawable.ic_notification_sync_error) - .setContentIntent(getDownloadLogsIntent(getApplicationContext())) - .setAutoCancel(true); - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - NotificationManager nm = (NotificationManager) getApplicationContext() - .getSystemService(Context.NOTIFICATION_SERVICE); - if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) - == PackageManager.PERMISSION_GRANTED) { - nm.notify(R.id.notification_download_report, builder.build()); - } - } - - private Notification generateProgressNotification() { - StringBuilder bigTextB = new StringBuilder(); - Map progressCopy; - synchronized (notificationProgress) { - progressCopy = new HashMap<>(notificationProgress); - } - for (Map.Entry entry : progressCopy.entrySet()) { - bigTextB.append(String.format(Locale.getDefault(), "%s (%d%%)\n", entry.getKey(), entry.getValue())); - } - String bigText = bigTextB.toString().trim(); - String contentText; - if (progressCopy.size() == 1) { - contentText = bigText; - } else { - contentText = getApplicationContext().getResources().getQuantityString(R.plurals.downloads_left, - progressCopy.size(), progressCopy.size()); - } - NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), - NotificationUtils.CHANNEL_ID_DOWNLOADING); - builder.setTicker(getApplicationContext().getString(R.string.download_notification_title_episodes)) - .setContentTitle(getApplicationContext().getString(R.string.download_notification_title_episodes)) - .setContentText(contentText) - .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) - .setContentIntent(getDownloadsIntent(getApplicationContext())) - .setAutoCancel(false) - .setOngoing(true) - .setWhen(0) - .setOnlyAlertOnce(true) - .setShowWhen(false) - .setSmallIcon(R.drawable.ic_notification_sync) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - return builder.build(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java deleted file mode 100644 index 5e2a82f33..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ /dev/null @@ -1,314 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import androidx.annotation.NonNull; -import android.text.TextUtils; -import android.util.Log; - -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.model.download.DownloadRequest; -import de.danoeh.antennapod.net.common.AntennapodHttpClient; -import okhttp3.CacheControl; -import okhttp3.internal.http.StatusLine; -import org.apache.commons.io.IOUtils; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.net.URI; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.parser.feed.util.DateUtils; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.core.util.StorageUtils; -import de.danoeh.antennapod.net.common.UriUtil; -import okhttp3.OkHttpClient; -import okhttp3.Protocol; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - -public class HttpDownloader extends Downloader { - private static final String TAG = "HttpDownloader"; - private static final int BUFFER_SIZE = 8 * 1024; - - public HttpDownloader(@NonNull DownloadRequest request) { - super(request); - } - - @Override - protected void download() { - File destination = new File(request.getDestination()); - final boolean fileExists = destination.exists(); - - RandomAccessFile out = null; - InputStream connection; - ResponseBody responseBody = null; - - try { - final URI uri = UriUtil.getURIFromRequestUrl(request.getSource()); - Request.Builder httpReq = new Request.Builder().url(uri.toURL()); - httpReq.tag(request); - httpReq.cacheControl(new CacheControl.Builder().noStore().build()); - - if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - // set header explicitly so that okhttp doesn't do transparent gzip - Log.d(TAG, "addHeader(\"Accept-Encoding\", \"identity\")"); - httpReq.addHeader("Accept-Encoding", "identity"); - httpReq.cacheControl(new CacheControl.Builder().noCache().build()); // noStore breaks CDNs - } - - if (uri.getScheme().equals("http")) { - httpReq.addHeader("Upgrade-Insecure-Requests", "1"); - } - - if (!TextUtils.isEmpty(request.getLastModified())) { - String lastModified = request.getLastModified(); - Date lastModifiedDate = DateUtils.parse(lastModified); - if (lastModifiedDate != null) { - long threeDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 3; - if (lastModifiedDate.getTime() > threeDaysAgo) { - Log.d(TAG, "addHeader(\"If-Modified-Since\", \"" + lastModified + "\")"); - httpReq.addHeader("If-Modified-Since", lastModified); - } - } else { - Log.d(TAG, "addHeader(\"If-None-Match\", \"" + lastModified + "\")"); - httpReq.addHeader("If-None-Match", lastModified); - } - } - - // add range header if necessary - if (fileExists && destination.length() > 0) { - request.setSoFar(destination.length()); - httpReq.addHeader("Range", "bytes=" + request.getSoFar() + "-"); - Log.d(TAG, "Adding range header: " + request.getSoFar()); - } - - Response response = newCall(httpReq); - responseBody = response.body(); - String contentEncodingHeader = response.header("Content-Encoding"); - boolean isGzip = false; - if (!TextUtils.isEmpty(contentEncodingHeader)) { - isGzip = TextUtils.equals(contentEncodingHeader.toLowerCase(), "gzip"); - } - - Log.d(TAG, "Response code is " + response.code()); - if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { - Log.d(TAG, "Feed '" + request.getSource() + "' not modified since last update, Download canceled"); - onCancelled(); - return; - } else if (!response.isSuccessful() || response.body() == null) { - callOnFailByResponseCode(response); - return; - } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA - && isContentTypeTextAndSmallerThan100kb(response)) { - onFail(DownloadError.ERROR_FILE_TYPE, null); - return; - } - checkIfRedirect(response); - - connection = new BufferedInputStream(responseBody.byteStream()); - - String contentRangeHeader = (fileExists) ? response.header("Content-Range") : null; - if (fileExists && response.code() == HttpURLConnection.HTTP_PARTIAL - && !TextUtils.isEmpty(contentRangeHeader)) { - String start = contentRangeHeader.substring("bytes ".length(), - contentRangeHeader.indexOf("-")); - request.setSoFar(Long.parseLong(start)); - Log.d(TAG, "Starting download at position " + request.getSoFar()); - - out = new RandomAccessFile(destination, "rw"); - out.seek(request.getSoFar()); - } else { - boolean success = destination.delete(); - success |= destination.createNewFile(); - if (!success) { - throw new IOException("Unable to recreate partially downloaded file"); - } - out = new RandomAccessFile(destination, "rw"); - } - - byte[] buffer = new byte[BUFFER_SIZE]; - int count; - request.setStatusMsg(R.string.download_running); - Log.d(TAG, "Getting size of download"); - request.setSize(responseBody.contentLength() + request.getSoFar()); - Log.d(TAG, "Size is " + request.getSize()); - if (request.getSize() < 0) { - request.setSize(DownloadResult.SIZE_UNKNOWN); - } - - long freeSpace = StorageUtils.getFreeSpaceAvailable(); - Log.d(TAG, "Free space is " + freeSpace); - if (request.getSize() != DownloadResult.SIZE_UNKNOWN && request.getSize() > freeSpace) { - onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null); - return; - } - - Log.d(TAG, "Starting download"); - try { - while (!cancelled && (count = connection.read(buffer)) != -1) { - out.write(buffer, 0, count); - request.setSoFar(request.getSoFar() + count); - int progressPercent = (int) (100.0 * request.getSoFar() / request.getSize()); - request.setProgressPercent(progressPercent); - } - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - if (cancelled) { - onCancelled(); - } else { - // check if size specified in the response header is the same as the size of the - // written file. This check cannot be made if compression was used - if (!isGzip && request.getSize() != DownloadResult.SIZE_UNKNOWN - && request.getSoFar() != request.getSize()) { - onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: " - + request.getSoFar() + " does not equal expected size " + request.getSize()); - return; - } else if (request.getSize() > 0 && request.getSoFar() == 0) { - onFail(DownloadError.ERROR_IO_ERROR, "Download completed, but nothing was read"); - return; - } - String lastModified = response.header("Last-Modified"); - if (lastModified != null) { - request.setLastModified(lastModified); - } else { - request.setLastModified(response.header("ETag")); - } - onSuccess(); - } - - } catch (IllegalArgumentException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage()); - } catch (SocketTimeoutException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage()); - } catch (UnknownHostException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage()); - } catch (IOException e) { - e.printStackTrace(); - if (NetworkUtils.wasDownloadBlocked(e)) { - onFail(DownloadError.ERROR_IO_BLOCKED, e.getMessage()); - return; - } - String message = e.getMessage(); - if (message != null && message.contains("Trust anchor for certification path not found")) { - onFail(DownloadError.ERROR_CERTIFICATE, e.getMessage()); - return; - } - onFail(DownloadError.ERROR_IO_ERROR, e.getMessage()); - } catch (NullPointerException e) { - // might be thrown by connection.getInputStream() - e.printStackTrace(); - onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); - } finally { - IOUtils.closeQuietly(out); - IOUtils.closeQuietly(responseBody); - } - } - - private Response newCall(Request.Builder httpReq) throws IOException { - OkHttpClient httpClient = AntennapodHttpClient.getHttpClient(); - try { - return httpClient.newCall(httpReq.build()).execute(); - } catch (IOException e) { - Log.e(TAG, e.toString()); - if (e.getMessage() != null && e.getMessage().contains("PROTOCOL_ERROR")) { - // Apparently some servers announce they support SPDY but then actually don't. - httpClient = httpClient.newBuilder() - .protocols(Collections.singletonList(Protocol.HTTP_1_1)) - .build(); - return httpClient.newCall(httpReq.build()).execute(); - } else { - throw e; - } - } - } - - private boolean isContentTypeTextAndSmallerThan100kb(Response response) { - int contentLength = -1; - String contentLen = response.header("Content-Length"); - if (contentLen != null) { - try { - contentLength = Integer.parseInt(contentLen); - } catch (NumberFormatException e) { - e.printStackTrace(); - } - } - Log.d(TAG, "content length: " + contentLength); - String contentType = response.header("Content-Type"); - Log.d(TAG, "content type: " + contentType); - return contentType != null && contentType.startsWith("text/") && contentLength < 100 * 1024; - } - - private void callOnFailByResponseCode(Response response) { - final DownloadError error; - final String details; - if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { - error = DownloadError.ERROR_UNAUTHORIZED; - details = String.valueOf(response.code()); - } else if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) { - error = DownloadError.ERROR_FORBIDDEN; - details = String.valueOf(response.code()); - } else if (response.code() == HttpURLConnection.HTTP_NOT_FOUND - || response.code() == HttpURLConnection.HTTP_GONE) { - error = DownloadError.ERROR_NOT_FOUND; - details = String.valueOf(response.code()); - } else { - error = DownloadError.ERROR_HTTP_DATA_ERROR; - details = String.valueOf(response.code()); - } - onFail(error, details); - } - - private void checkIfRedirect(Response response) { - // detect 301 Moved permanently and 308 Permanent Redirect - ArrayList responses = new ArrayList<>(); - while (response != null) { - responses.add(response); - response = response.priorResponse(); - } - if (responses.size() < 2) { - return; - } - Collections.reverse(responses); - int firstCode = responses.get(0).code(); - String firstUrl = responses.get(0).request().url().toString(); - String secondUrl = responses.get(1).request().url().toString(); - if (firstCode == HttpURLConnection.HTTP_MOVED_PERM || firstCode == StatusLine.HTTP_PERM_REDIRECT) { - Log.d(TAG, "Detected permanent redirect from " + request.getSource() + " to " + secondUrl); - permanentRedirectUrl = secondUrl; - } else if (secondUrl.equals(firstUrl.replace("http://", "https://"))) { - Log.d(TAG, "Treating http->https non-permanent redirect as permanent: " + firstUrl); - permanentRedirectUrl = secondUrl; - } - } - - private void onSuccess() { - Log.d(TAG, "Download was successful"); - result.setSuccessful(); - } - - private void onFail(DownloadError reason, String reasonDetailed) { - Log.d(TAG, "onFail() called with: " + "reason = [" + reason + "], reasonDetailed = [" + reasonDetailed + "]"); - result.setFailed(reason, reasonDetailed); - } - - private void onCancelled() { - Log.d(TAG, "Download was cancelled"); - result.setCancelled(); - cancelled = true; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java deleted file mode 100644 index eff5816be..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java +++ /dev/null @@ -1,147 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import android.Manifest; -import android.app.Notification; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; - -import android.graphics.Bitmap; -import android.os.Build; -import android.util.Log; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.ContextCompat; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedCounter; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.storage.database.PodDBAdapter; - -import de.danoeh.antennapod.ui.notifications.NotificationUtils; -import java.util.Map; - -public class NewEpisodesNotification { - private static final String TAG = "NewEpisodesNotification"; - private static final String GROUP_KEY = "de.danoeh.antennapod.EPISODES"; - - private Map countersBefore; - - public NewEpisodesNotification() { - } - - public void loadCountersBeforeRefresh() { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - countersBefore = adapter.getFeedCounters(FeedCounter.SHOW_NEW); - adapter.close(); - } - - public void showIfNeeded(Context context, Feed feed) { - FeedPreferences prefs = feed.getPreferences(); - if (!prefs.getKeepUpdated() || !prefs.getShowEpisodeNotification()) { - return; - } - - int newEpisodesBefore = countersBefore.containsKey(feed.getId()) ? countersBefore.get(feed.getId()) : 0; - int newEpisodesAfter = getNewEpisodeCount(feed.getId()); - - Log.d(TAG, "New episodes before: " + newEpisodesBefore + ", after: " + newEpisodesAfter); - if (newEpisodesAfter > newEpisodesBefore) { - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - showNotification(newEpisodesAfter, feed, context, notificationManager); - } - } - - private static void showNotification(int newEpisodes, Feed feed, Context context, - NotificationManagerCompat notificationManager) { - Resources res = context.getResources(); - String text = res.getQuantityString( - R.plurals.new_episode_notification_message, newEpisodes, newEpisodes, feed.getTitle() - ); - String title = res.getQuantityString(R.plurals.new_episode_notification_title, newEpisodes); - - Intent intent = new Intent(); - intent.setAction("NewEpisodes" + feed.getId()); - intent.setComponent(new ComponentName(context, "de.danoeh.antennapod.activity.MainActivity")); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra("fragment_feed_id", feed.getId()); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, - (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - - Notification notification = new NotificationCompat.Builder( - context, NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS) - .setSmallIcon(R.drawable.ic_notification_new) - .setContentTitle(title) - .setLargeIcon(loadIcon(context, feed)) - .setContentText(text) - .setContentIntent(pendingIntent) - .setGroup(GROUP_KEY) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) - .setOnlyAlertOnce(true) - .setAutoCancel(true) - .build(); - if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) - == PackageManager.PERMISSION_GRANTED) { - notificationManager.notify(NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS, - feed.hashCode(), notification); - } - showGroupSummaryNotification(context, notificationManager); - } - - private static void showGroupSummaryNotification(Context context, NotificationManagerCompat notificationManager) { - Intent intent = new Intent(); - intent.setAction("NewEpisodes"); - intent.setComponent(new ComponentName(context, "de.danoeh.antennapod.activity.MainActivity")); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra("fragment_tag", "NewEpisodesFragment"); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, - (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - - Notification notificationGroupSummary = new NotificationCompat.Builder( - context, NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS) - .setSmallIcon(R.drawable.ic_notification_new) - .setContentTitle(context.getString(R.string.new_episode_notification_group_text)) - .setContentIntent(pendingIntent) - .setGroup(GROUP_KEY) - .setGroupSummary(true) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) - .setOnlyAlertOnce(true) - .setAutoCancel(true) - .build(); - if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) - == PackageManager.PERMISSION_GRANTED) { - notificationManager.notify(NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS, - 0, notificationGroupSummary); - } - } - - private static Bitmap loadIcon(Context context, Feed feed) { - int iconSize = (int) (128 * context.getResources().getDisplayMetrics().density); - try { - return Glide.with(context) - .asBitmap() - .load(feed.getImageUrl()) - .apply(new RequestOptions().centerCrop()) - .submit(iconSize, iconSize) - .get(); - } catch (Throwable tr) { - return null; - } - } - - private static int getNewEpisodeCount(long feedId) { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - Map counters = adapter.getFeedCounters(FeedCounter.SHOW_NEW, feedId); - int episodeCount = counters.containsKey(feedId) ? counters.get(feedId) : 0; - adapter.close(); - return episodeCount; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java deleted file mode 100644 index 37775ab94..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java +++ /dev/null @@ -1,125 +0,0 @@ -package de.danoeh.antennapod.core.service.download.handler; - -import android.text.TextUtils; -import android.util.Log; -import androidx.annotation.NonNull; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; -import de.danoeh.antennapod.model.download.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.parser.feed.FeedHandler; -import de.danoeh.antennapod.parser.feed.FeedHandlerResult; -import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.core.util.InvalidFeedException; -import org.xml.sax.SAXException; - -import javax.xml.parsers.ParserConfigurationException; -import java.io.File; -import java.io.IOException; -import java.util.Date; -import java.util.concurrent.Callable; - -public class FeedParserTask implements Callable { - private static final String TAG = "FeedParserTask"; - private final DownloadRequest request; - private DownloadResult downloadResult; - private boolean successful = true; - - public FeedParserTask(DownloadRequest request) { - this.request = request; - downloadResult = new DownloadResult( - 0, request.getTitle(), 0, request.getFeedfileType(), false, - DownloadError.ERROR_REQUEST_ERROR, new Date(), - "Unknown error: Status not set"); - } - - @Override - public FeedHandlerResult call() { - Feed feed = new Feed(request.getSource(), request.getLastModified()); - feed.setLocalFileUrl(request.getDestination()); - feed.setId(request.getFeedfileId()); - feed.setPreferences(new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, - VolumeAdaptionSetting.OFF, FeedPreferences.NewEpisodesAction.GLOBAL, request.getUsername(), - request.getPassword())); - feed.setPageNr(request.getArguments().getInt(DownloadRequest.REQUEST_ARG_PAGE_NR, 0)); - - DownloadError reason = null; - String reasonDetailed = null; - FeedHandler feedHandler = new FeedHandler(); - - FeedHandlerResult result = null; - try { - result = feedHandler.parseFeed(feed); - Log.d(TAG, feed.getTitle() + " parsed"); - checkFeedData(feed); - if (TextUtils.isEmpty(feed.getImageUrl())) { - feed.setImageUrl(Feed.PREFIX_GENERATIVE_COVER + feed.getDownloadUrl()); - } - } catch (SAXException | IOException | ParserConfigurationException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (UnsupportedFeedtypeException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_UNSUPPORTED_TYPE; - if ("html".equalsIgnoreCase(e.getRootElement())) { - reason = DownloadError.ERROR_UNSUPPORTED_TYPE_HTML; - } - reasonDetailed = e.getMessage(); - } catch (InvalidFeedException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } finally { - File feedFile = new File(request.getDestination()); - if (feedFile.exists()) { - boolean deleted = feedFile.delete(); - Log.d(TAG, "Deletion of file '" + feedFile.getAbsolutePath() + "' " - + (deleted ? "successful" : "FAILED")); - } - } - - if (successful) { - downloadResult = new DownloadResult(feed.getHumanReadableIdentifier(), feed.getId(), - Feed.FEEDFILETYPE_FEED, true, DownloadError.SUCCESS, reasonDetailed); - return result; - } else { - downloadResult = new DownloadResult(feed.getHumanReadableIdentifier(), feed.getId(), - Feed.FEEDFILETYPE_FEED, false, reason, reasonDetailed); - return null; - } - } - - public boolean isSuccessful() { - return successful; - } - - /** - * Checks if the feed was parsed correctly. - */ - private void checkFeedData(Feed feed) throws InvalidFeedException { - if (feed.getTitle() == null) { - throw new InvalidFeedException("Feed has no title"); - } - checkFeedItems(feed); - } - - private void checkFeedItems(Feed feed) throws InvalidFeedException { - for (FeedItem item : feed.getItems()) { - if (item.getTitle() == null) { - throw new InvalidFeedException("Item has no title: " + item); - } - } - } - - @NonNull - public DownloadResult getDownloadStatus() { - return downloadResult; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java deleted file mode 100644 index 24b157c88..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java +++ /dev/null @@ -1,119 +0,0 @@ -package de.danoeh.antennapod.core.service.download.handler; - -import android.content.Context; -import android.media.MediaMetadataRetriever; -import android.util.Log; - -import androidx.annotation.NonNull; - -import de.danoeh.antennapod.model.MediaMetadataRetrieverCompat; -import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; -import org.greenrobot.eventbus.EventBus; - -import java.io.File; -import java.io.InterruptedIOException; -import java.util.concurrent.ExecutionException; - -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.model.download.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; - -/** - * Handles a completed media download. - */ -public class MediaDownloadedHandler implements Runnable { - private static final String TAG = "MediaDownloadedHandler"; - private final DownloadRequest request; - private final Context context; - private DownloadResult updatedStatus; - - public MediaDownloadedHandler(@NonNull Context context, @NonNull DownloadResult status, - @NonNull DownloadRequest request) { - this.request = request; - this.context = context; - this.updatedStatus = status; - } - - @Override - public void run() { - FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId()); - if (media == null) { - Log.e(TAG, "Could not find downloaded media object in database"); - return; - } - // media.setDownloaded modifies played state - boolean broadcastUnreadStateUpdate = media.getItem() != null && media.getItem().isNew(); - media.setDownloaded(true); - media.setLocalFileUrl(request.getDestination()); - media.setSize(new File(request.getDestination()).length()); - media.checkEmbeddedPicture(); // enforce check - - try { - // Cache chapters if file has them - if (media.getItem() != null && !media.getItem().hasChapters()) { - media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context)); - } - if (media.getItem() != null && media.getItem().getPodcastIndexChapterUrl() != null) { - ChapterUtils.loadChaptersFromUrl(media.getItem().getPodcastIndexChapterUrl(), false); - } - } catch (InterruptedIOException ignore) { - // Ignore - } - - // Get duration - String durationStr = null; - try (MediaMetadataRetrieverCompat mmr = new MediaMetadataRetrieverCompat()) { - mmr.setDataSource(media.getLocalFileUrl()); - durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - media.setDuration(Integer.parseInt(durationStr)); - Log.d(TAG, "Duration of file is " + media.getDuration()); - } catch (NumberFormatException e) { - Log.d(TAG, "Invalid file duration: " + durationStr); - } catch (Exception e) { - Log.e(TAG, "Get duration failed", e); - } - - final FeedItem item = media.getItem(); - - try { - DBWriter.setFeedMedia(media).get(); - - // we've received the media, we don't want to autodownload it again - if (item != null) { - item.disableAutoDownload(); - // setFeedItem() signals (via EventBus) that the item has been updated, - // so we do it after the enclosing media has been updated above, - // to ensure subscribers will get the updated FeedMedia as well - DBWriter.setFeedItem(item).get(); - if (broadcastUnreadStateUpdate) { - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - } - } - } catch (InterruptedException e) { - Log.e(TAG, "MediaHandlerThread was interrupted"); - } catch (ExecutionException e) { - Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.getMessage()); - updatedStatus = new DownloadResult(media.getEpisodeTitle(), media.getId(), - FeedMedia.FEEDFILETYPE_FEEDMEDIA, false, DownloadError.ERROR_DB_ACCESS_ERROR, e.getMessage()); - } - - if (item != null) { - EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD) - .currentTimestamp() - .build(); - SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action); - } - } - - @NonNull - public DownloadResult getUpdatedStatus() { - return updatedStatus; - } -} 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 deleted file mode 100644 index a86bf0bcf..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FastDocumentFile.java +++ /dev/null @@ -1,72 +0,0 @@ -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.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) { - 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/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java deleted file mode 100644 index 69c23efc2..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java +++ /dev/null @@ -1,76 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import android.text.TextUtils; - -import androidx.annotation.VisibleForTesting; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; - -import java.io.UnsupportedEncodingException; -import java.security.NoSuchAlgorithmException; - - -/** Generates valid filenames for a given string. */ -public class FileNameGenerator { - @VisibleForTesting - public static final int MAX_FILENAME_LENGTH = 242; // limited by CircleCI - private static final int MD5_HEX_LENGTH = 32; - - private static final char[] validChars = - ("abcdefghijklmnopqrstuvwxyz" - + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - + "0123456789" - + " _-").toCharArray(); - - private FileNameGenerator() { - } - - /** - * This method will return a new string that doesn't contain any illegal - * characters of the given string. - */ - public static String generateFileName(String string) { - string = StringUtils.stripAccents(string); - StringBuilder buf = new StringBuilder(); - for (int i = 0; i < string.length(); i++) { - char c = string.charAt(i); - if (Character.isSpaceChar(c) - && (buf.length() == 0 || Character.isSpaceChar(buf.charAt(buf.length() - 1)))) { - continue; - } - if (ArrayUtils.contains(validChars, c)) { - buf.append(c); - } - } - String filename = buf.toString().trim(); - if (TextUtils.isEmpty(filename)) { - return randomString(8); - } else if (filename.length() >= MAX_FILENAME_LENGTH) { - return filename.substring(0, MAX_FILENAME_LENGTH - MD5_HEX_LENGTH - 1) + "_" + md5(filename); - } else { - return filename; - } - } - - private static String randomString(int length) { - StringBuilder sb = new StringBuilder(length); - for (int i = 0; i < length; i++) { - sb.append(validChars[(int) (Math.random() * validChars.length)]); - } - return sb.toString(); - } - - private static String md5(String md5) { - try { - java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); - byte[] array = md.digest(md5.getBytes("UTF-8")); - StringBuilder sb = new StringBuilder(); - for (byte b : array) { - sb.append(Integer.toHexString((b & 0xFF) | 0x100).substring(1, 3)); - } - return sb.toString(); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { - return null; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java b/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java deleted file mode 100644 index a45136432..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.danoeh.antennapod.core.util; - -/** - * Thrown if a feed has invalid attribute values. - */ -public class InvalidFeedException extends Exception { - private static final long serialVersionUID = 1L; - - public InvalidFeedException(String message) { - super(message); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManagerImpl.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManagerImpl.java deleted file mode 100644 index 17077c237..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManagerImpl.java +++ /dev/null @@ -1,118 +0,0 @@ -package de.danoeh.antennapod.core.util.download; - -import android.content.Context; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.work.Constraints; -import androidx.work.Data; -import androidx.work.ExistingPeriodicWorkPolicy; -import androidx.work.ExistingWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.OutOfQuotaPolicy; -import androidx.work.PeriodicWorkRequest; -import androidx.work.WorkManager; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.service.FeedUpdateWorker; -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import org.greenrobot.eventbus.EventBus; - -import java.util.concurrent.TimeUnit; - -public class FeedUpdateManagerImpl extends FeedUpdateManager { - public static final String WORK_TAG_FEED_UPDATE = "feedUpdate"; - private static final String WORK_ID_FEED_UPDATE = "de.danoeh.antennapod.core.service.FeedUpdateWorker"; - private static final String WORK_ID_FEED_UPDATE_MANUAL = "feedUpdateManual"; - public static final String EXTRA_FEED_ID = "feed_id"; - public static final String EXTRA_NEXT_PAGE = "next_page"; - public static final String EXTRA_EVEN_ON_MOBILE = "even_on_mobile"; - private static final String TAG = "AutoUpdateManager"; - - /** - * Start / restart periodic auto feed refresh - * @param context Context - */ - public void restartUpdateAlarm(Context context, boolean replace) { - if (UserPreferences.isAutoUpdateDisabled()) { - WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE); - } else { - PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder( - FeedUpdateWorker.class, UserPreferences.getUpdateInterval(), TimeUnit.HOURS) - .setConstraints(new Constraints.Builder() - .setRequiredNetworkType(UserPreferences.isAllowMobileFeedRefresh() - ? NetworkType.CONNECTED : NetworkType.UNMETERED).build()) - .build(); - WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE, - replace ? ExistingPeriodicWorkPolicy.REPLACE : ExistingPeriodicWorkPolicy.KEEP, workRequest); - } - } - - public void runOnce(Context context) { - runOnce(context, null, false); - } - - public void runOnce(Context context, Feed feed) { - runOnce(context, feed, false); - } - - public void runOnce(Context context, Feed feed, boolean nextPage) { - OneTimeWorkRequest.Builder workRequest = new OneTimeWorkRequest.Builder(FeedUpdateWorker.class) - .setInitialDelay(0L, TimeUnit.MILLISECONDS) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) - .addTag(WORK_TAG_FEED_UPDATE); - if (feed == null || !feed.isLocalFeed()) { - workRequest.setConstraints(new Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED).build()); - } - Data.Builder builder = new Data.Builder(); - builder.putBoolean(EXTRA_EVEN_ON_MOBILE, true); - if (feed != null) { - builder.putLong(EXTRA_FEED_ID, feed.getId()); - builder.putBoolean(EXTRA_NEXT_PAGE, nextPage); - } - workRequest.setInputData(builder.build()); - WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE_MANUAL, - ExistingWorkPolicy.REPLACE, workRequest.build()); - } - - public void runOnceOrAsk(@NonNull Context context) { - runOnceOrAsk(context, null); - } - - public void runOnceOrAsk(@NonNull Context context, @Nullable Feed feed) { - Log.d(TAG, "Run auto update immediately in background."); - if (feed != null && feed.isLocalFeed()) { - runOnce(context, feed); - } else if (!NetworkUtils.networkAvailable()) { - EventBus.getDefault().post(new MessageEvent(context.getString(R.string.download_error_no_connection))); - } else if (NetworkUtils.isFeedRefreshAllowed()) { - runOnce(context, feed); - } else { - confirmMobileRefresh(context, feed); - } - } - - private void confirmMobileRefresh(final Context context, @Nullable Feed feed) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) - .setTitle(R.string.feed_refresh_title) - .setPositiveButton(R.string.confirm_mobile_streaming_button_once, - (dialog, which) -> runOnce(context, feed)) - .setNeutralButton(R.string.confirm_mobile_streaming_button_always, (dialog, which) -> { - UserPreferences.setAllowMobileFeedRefresh(true); - runOnce(context, feed); - }) - .setNegativeButton(R.string.no, null); - if (NetworkUtils.isNetworkRestricted() && NetworkUtils.isVpnOverWifi()) { - builder.setMessage(R.string.confirm_mobile_feed_refresh_dialog_message_vpn); - } else { - builder.setMessage(R.string.confirm_mobile_feed_refresh_dialog_message); - } - builder.show(); - } -} diff --git a/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml index 7bb78c1c9..8678b4413 100644 --- a/core/src/main/res/values/ids.xml +++ b/core/src/main/res/values/ids.xml @@ -16,10 +16,6 @@ - - - - \ No newline at end of file diff --git a/core/src/test/assets/local-feed1/track1.mp3 b/core/src/test/assets/local-feed1/track1.mp3 deleted file mode 100644 index b1f993c3f..000000000 Binary files a/core/src/test/assets/local-feed1/track1.mp3 and /dev/null differ diff --git a/core/src/test/assets/local-feed2/folder.png b/core/src/test/assets/local-feed2/folder.png deleted file mode 100644 index 9e522a986..000000000 Binary files a/core/src/test/assets/local-feed2/folder.png and /dev/null differ diff --git a/core/src/test/assets/local-feed2/track1.mp3 b/core/src/test/assets/local-feed2/track1.mp3 deleted file mode 100644 index b1f993c3f..000000000 Binary files a/core/src/test/assets/local-feed2/track1.mp3 and /dev/null differ diff --git a/core/src/test/assets/local-feed2/track2.mp3 b/core/src/test/assets/local-feed2/track2.mp3 deleted file mode 100644 index 310cddd6b..000000000 Binary files a/core/src/test/assets/local-feed2/track2.mp3 and /dev/null differ 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 deleted file mode 100644 index 9703894f5..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java +++ /dev/null @@ -1,307 +0,0 @@ -package de.danoeh.antennapod.core.feed; - -import android.content.Context; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.webkit.MimeTypeMap; - -import androidx.annotation.NonNull; -import androidx.test.platform.app.InstrumentationRegistry; - -import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterfaceStub; -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; -import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.shadows.ShadowMediaMetadataRetriever; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; - -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.any; -import static org.robolectric.Shadows.shadowOf; - -/** - * Test local feeds handling in class LocalFeedUpdater. - */ -@RunWith(RobolectricTestRunner.class) -public class LocalFeedUpdaterTest { - - /** - * URL to locate the local feed media files on the external storage (SD card). - * The exact URL doesn't matter here as access to external storage is mocked - * (seems not to be supported by Robolectric). - */ - private static final String FEED_URL = - "content://com.android.externalstorage.documents/tree/primary%3ADownload%2Flocal-feed"; - 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; - - @Before - public void setUp() throws Exception { - // Initialize environment - context = InstrumentationRegistry.getInstrumentation().getContext(); - UserPreferences.init(context); - PlaybackPreferences.init(context); - SynchronizationSettings.init(context); - DownloadServiceInterface.setImpl(new DownloadServiceInterfaceStub()); - - // Initialize database - PodDBAdapter.init(context); - PodDBAdapter.deleteDatabase(); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.close(); - - mapDummyMetadata(LOCAL_FEED_DIR1); - mapDummyMetadata(LOCAL_FEED_DIR2); - shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("mp3", "audio/mp3"); - } - - @After - public void tearDown() { - DBWriter.tearDownTests(); - PodDBAdapter.tearDownTests(); - } - - /** - * Test adding a new local feed. - */ - @Test - public void testUpdateFeed_AddNewFeed() { - // check for empty database - List feedListBefore = DBReader.getFeedList(); - assertThat(feedListBefore, is(empty())); - - callUpdateFeed(LOCAL_FEED_DIR2); - - // verify new feed in database - verifySingleFeedInDatabaseAndItemCount(2); - Feed feedAfter = verifySingleFeedInDatabase(); - assertEquals(FEED_URL, feedAfter.getDownloadUrl()); - } - - /** - * Test adding further items to an existing local feed. - */ - @Test - public void testUpdateFeed_AddMoreItems() { - // add local feed with 1 item (localFeedDir1) - callUpdateFeed(LOCAL_FEED_DIR1); - - // now add another item (by changing to local feed folder localFeedDir2) - callUpdateFeed(LOCAL_FEED_DIR2); - - verifySingleFeedInDatabaseAndItemCount(2); - } - - /** - * Test removing items from an existing local feed without a corresponding media file. - */ - @Test - public void testUpdateFeed_RemoveItems() { - // add local feed with 2 items (localFeedDir1) - callUpdateFeed(LOCAL_FEED_DIR2); - - // now remove an item (by changing to local feed folder localFeedDir1) - callUpdateFeed(LOCAL_FEED_DIR1); - - verifySingleFeedInDatabaseAndItemCount(1); - } - - /** - * Test feed icon defined in the local feed media folder. - */ - @Test - public void testUpdateFeed_FeedIconFromFolder() { - callUpdateFeed(LOCAL_FEED_DIR2); - - Feed feedAfter = verifySingleFeedInDatabase(); - assertThat(feedAfter.getImageUrl(), endsWith("local-feed2/folder.png")); - } - - /** - * Test default feed icon if there is no matching file in the local feed media folder. - */ - @Test - public void testUpdateFeed_FeedIconDefault() { - callUpdateFeed(LOCAL_FEED_DIR1); - - Feed feedAfter = verifySingleFeedInDatabase(); - assertThat(feedAfter.getImageUrl(), startsWith(Feed.PREFIX_GENERATIVE_COVER)); - } - - /** - * Test default feed metadata. - * - * @see #mapDummyMetadata Title and PubDate are dummy values. - */ - @Test - public void testUpdateFeed_FeedMetadata() { - callUpdateFeed(LOCAL_FEED_DIR1); - - Feed feed = verifySingleFeedInDatabase(); - List feedItems = DBReader.getFeedItemList(feed); - assertEquals("track1.mp3", feedItems.get(0).getTitle()); - } - - @Test - public void testGetImageUrl_EmptyFolder() { - String imageUrl = LocalFeedUpdater.getImageUrl(Collections.emptyList(), Uri.EMPTY); - assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER)); - } - - @Test - public void testGetImageUrl_NoImageButAudioFiles() { - 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) { - List folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"), - mockDocumentFile(filename, "image/jpeg")); // image MIME type doesn't matter - String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); - assertThat(imageUrl, endsWith(filename)); - } - } - - @Test - public void testGetImageUrl_OtherImageFilenameJpg() { - List folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"), - mockDocumentFile("my-image.jpg", "image/jpeg")); - String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); - assertThat(imageUrl, endsWith("my-image.jpg")); - } - - @Test - public void testGetImageUrl_OtherImageFilenameJpeg() { - List folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"), - mockDocumentFile("my-image.jpeg", "image/jpeg")); - String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); - assertThat(imageUrl, endsWith("my-image.jpeg")); - } - - @Test - public void testGetImageUrl_OtherImageFilenamePng() { - List folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"), - mockDocumentFile("my-image.png", "image/png")); - String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); - assertThat(imageUrl, endsWith("my-image.png")); - } - - @Test - public void testGetImageUrl_OtherImageFilenameUnsupportedMimeType() { - List folder = Arrays.asList(mockDocumentFile("audio.mp3", "audio/mp3"), - mockDocumentFile("my-image.svg", "image/svg+xml")); - String imageUrl = LocalFeedUpdater.getImageUrl(folder, Uri.EMPTY); - assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER)); - } - - /** - * Fill ShadowMediaMetadataRetriever with dummy duration and title. - * - * @param localFeedDir assets local feed folder with media files - */ - 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"); - ShadowMediaMetadataRetriever.addMetadata(path, - MediaMetadataRetriever.METADATA_KEY_TITLE, fileName); - ShadowMediaMetadataRetriever.addMetadata(path, - MediaMetadataRetriever.METADATA_KEY_DATE, "20200601T222324"); - } - } - - /** - * 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) { - try (MockedStatic dfMock = Mockito.mockStatic(FastDocumentFile.class)) { - // mock external storage - dfMock.when(() -> FastDocumentFile.list(any(), any())).thenReturn(mockLocalFolder(localFeedDir)); - - // call method to test - Feed feed = new Feed(FEED_URL, null); - try { - LocalFeedUpdater.tryUpdateFeed(feed, context, null, null); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - /** - * Verify that the database contains exactly one feed and return that feed. - */ - @NonNull - private static Feed verifySingleFeedInDatabase() { - List feedListAfter = DBReader.getFeedList(); - assertEquals(1, feedListAfter.size()); - return feedListAfter.get(0); - } - - /** - * Verify that the database contains exactly one feed and the number of - * items in the feed. - * - * @param expectedItemCount expected number of items in the feed - */ - private static void verifySingleFeedInDatabaseAndItemCount(int expectedItemCount) { - Feed feed = verifySingleFeedInDatabase(); - List feedItems = DBReader.getFeedItemList(feed); - assertEquals(expectedItemCount, feedItems.size()); - } - - /** - * Create a DocumentFile mock object. - */ - @NonNull - private static FastDocumentFile mockDocumentFile(@NonNull String fileName, @NonNull String mimeType) { - return new FastDocumentFile(fileName, mimeType, Uri.parse("file:///path/" + fileName), 0, 0); - } - - 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; - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/FilenameGeneratorTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/FilenameGeneratorTest.java index af22a4b9d..4c225322a 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/util/FilenameGeneratorTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/util/FilenameGeneratorTest.java @@ -5,6 +5,7 @@ import android.text.TextUtils; import java.io.File; +import de.danoeh.antennapod.net.download.serviceinterface.FileNameGenerator; import org.apache.commons.lang3.StringUtils; import org.junit.Test; import org.junit.runner.RunWith; -- cgit v1.2.3