From f47134a7ebb18beb64ec1e6dd53d860d40698db9 Mon Sep 17 00:00:00 2001 From: schasi <5891239+schasi@users.noreply.github.com> Date: Sun, 30 Nov 2025 23:02:47 +0100 Subject: [PATCH] Open podcast or episode from download log details (#7867) --- .../test/antennapod/ui/DownloadLogTest.java | 126 +++++++++++ .../antennapod/activity/MainActivity.java | 2 +- .../download/CompletedDownloadsFragment.java | 4 +- .../download/DownloadLogDetailsDialog.java | 212 +++++++++++++----- .../screen/download/DownloadLogFragment.java | 11 +- .../ui/screen/feed/FeedItemlistFragment.java | 5 +- .../layout/download_log_details_dialog.xml | 154 +++++++++++++ .../model/download/DownloadResult.java | 5 +- ui/i18n/src/main/res/values/strings.xml | 6 + 9 files changed, 457 insertions(+), 68 deletions(-) create mode 100644 app/src/androidTest/java/de/test/antennapod/ui/DownloadLogTest.java create mode 100644 app/src/main/res/layout/download_log_details_dialog.xml diff --git a/app/src/androidTest/java/de/test/antennapod/ui/DownloadLogTest.java b/app/src/androidTest/java/de/test/antennapod/ui/DownloadLogTest.java new file mode 100644 index 000000000..8a40885ae --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/ui/DownloadLogTest.java @@ -0,0 +1,126 @@ +package de.test.antennapod.ui; + +import android.content.Context; +import android.content.Intent; +import androidx.test.espresso.intent.rule.IntentsTestRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +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.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment; +import de.test.antennapod.EspressoTestUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.ExecutionException; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isRoot; +import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static de.test.antennapod.EspressoTestUtils.waitForView; +import static de.test.antennapod.EspressoTestUtils.waitForViewGlobally; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.allOf; + +@RunWith(AndroidJUnit4.class) +public class DownloadLogTest { + @Rule + public IntentsTestRule activityRule = new IntentsTestRule<>(MainActivity.class, false, false); + private Context context; + private Intent completedDownloadsIntent; + private Feed feed; + private FeedMedia media; + + @Before + public void setUp() throws Exception { + EspressoTestUtils.clearPreferences(); + EspressoTestUtils.clearDatabase(); + context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + completedDownloadsIntent = new Intent(context, MainActivity.class); + completedDownloadsIntent.putExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG, CompletedDownloadsFragment.TAG); + + feed = new Feed(0, "last modified", "@@Feed title@@", "link", "description", "payment link", + "@author@", "language", "type", "feedIdentifier", "http://localhost/cover.png", + "/sdcard/abc", "http://localhost/feed.xml", 0); + FeedItem item = new FeedItem(0, "title", "identifier", "link", new Date(), FeedItem.UNPLAYED, feed); + media = new FeedMedia(item, "http://localhost/media.mp3", 10000, "mime type"); + item.setMedia(media); + feed.setItems(Collections.singletonList(item)); + feed = FeedDatabaseWriter.updateFeed(context, feed, false); + } + + @Test + public void testExistingSubscribedFeed() { + DownloadResult result = new DownloadResult("@@Title@@", feed.getId(), + Feed.FEEDFILETYPE_FEED, false, DownloadError.ERROR_IO_ERROR, "@@reason@@"); + openDialog(result); + // Open feed + onView(withText(R.string.download_log_open_feed)).perform(click()); + waitForViewGlobally(withText(feed.getAuthor()), 2000); + } + + @Test + public void testExistingNonSubscribedFeed() throws InterruptedException, ExecutionException { + DBWriter.setFeedState(context, feed, Feed.STATE_NOT_SUBSCRIBED).get(); + DownloadResult result = new DownloadResult("@@Title@@", feed.getId(), + Feed.FEEDFILETYPE_FEED, false, DownloadError.ERROR_IO_ERROR, "@@reason@@"); + openDialog(result); + // Opens online feed view + onView(withText(R.string.download_log_open_feed)).perform(click()); + waitForViewGlobally(withText(feed.getAuthor()), 2000); + onView(isRoot()).perform(waitForView(allOf(withText(R.string.subscribe_label), isDisplayed()), 2000)); + } + + @Test + public void testNonExistingFeed() { + DownloadResult result = new DownloadResult("@@Title@@", feed.getId() + 1, + Feed.FEEDFILETYPE_FEED, false, DownloadError.ERROR_IO_ERROR, "@@reason@@"); + openDialog(result); + // Does not have button + onView(withText(R.string.download_log_open_feed)).check(matches(not(isDisplayed()))); + } + + @Test + public void testExistingMedia() { + DownloadResult result = new DownloadResult("@@Title@@", media.getId(), + FeedMedia.FEEDFILETYPE_FEEDMEDIA, false, DownloadError.ERROR_IO_ERROR, "@@reason@@"); + openDialog(result); + // Opens feed + onView(withText(R.string.download_log_open_feed)).perform(click()); + waitForViewGlobally(withText(feed.getAuthor()), 2000); + } + + @Test + public void testNonExistingMedia() { + DownloadResult result = new DownloadResult("@@Title@@", media.getId() + 1, + FeedMedia.FEEDFILETYPE_FEEDMEDIA, false, DownloadError.ERROR_IO_ERROR, "@@reason@@"); + openDialog(result); + // Does not have button + onView(withText(R.string.download_log_open_feed)).check(matches(not(isDisplayed()))); + } + + void openDialog(DownloadResult result) { + DBWriter.addDownloadStatus(result); + activityRule.launchActivity(completedDownloadsIntent); + onView(withContentDescription(R.string.downloads_log_label)).perform(click()); + onView(isRoot()).perform(waitForView(allOf(withText(result.getTitle()), isDisplayed()), 1000)); + onView(withText(result.getTitle())).perform(click()); + onView(isRoot()).perform(waitForView(allOf(withText(result.getReasonDetailed()), isDisplayed()), 1000)); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index 09687d4ac..9b124a26d 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -707,7 +707,7 @@ public class MainActivity extends CastEnabledActivity { drawerLayout.open(); } if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DOWNLOAD_LOGS, false)) { - new DownloadLogFragment().show(getSupportFragmentManager(), null); + new DownloadLogFragment().show(getSupportFragmentManager(), DownloadLogFragment.TAG); } if (intent.getBooleanExtra(EXTRA_REFRESH_ON_START, false)) { FeedUpdateManager.getInstance().runOnceOrAsk(this); diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/CompletedDownloadsFragment.java index ab0e28596..db59a2612 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/CompletedDownloadsFragment.java @@ -131,7 +131,7 @@ public class CompletedDownloadsFragment extends Fragment return true; }); if (getArguments() != null && getArguments().getBoolean(ARG_SHOW_LOGS, false)) { - new DownloadLogFragment().show(getChildFragmentManager(), null); + new DownloadLogFragment().show(getChildFragmentManager(), DownloadLogFragment.TAG); } addEmptyView(); @@ -176,7 +176,7 @@ public class CompletedDownloadsFragment extends Fragment FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); return true; } else if (item.getItemId() == R.id.action_download_logs) { - new DownloadLogFragment().show(getChildFragmentManager(), null); + new DownloadLogFragment().show(getChildFragmentManager(), DownloadLogFragment.TAG); return true; } else if (item.getItemId() == R.id.action_search) { ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogDetailsDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogDetailsDialog.java index c8c96a968..48f8585f0 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogDetailsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogDetailsDialog.java @@ -1,71 +1,169 @@ package de.danoeh.antennapod.ui.screen.download; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.os.Build; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.style.ForegroundColorSpan; -import android.widget.TextView; +import android.app.Dialog; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.View; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.DownloadLogDetailsDialogBinding; import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedMedia; -import org.greenrobot.eventbus.EventBus; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter; +import de.danoeh.antennapod.ui.common.ClipboardUtils; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; -public class DownloadLogDetailsDialog extends MaterialAlertDialogBuilder { +/** + * Shows a dialog with Feed title (and FeedItem title if possible). + * Can show a button to jump to the feed details view. + */ +public class DownloadLogDetailsDialog extends DialogFragment { + public static final String TAG = "DownloadLogDetails"; + private static final String EXTRA_IS_JUMP_TO_FEED = "isJumpToFeed"; + private static final String EXTRA_DOWNLOAD_RESULT = "downloadResult"; + private DownloadLogDetailsDialogBinding viewBinding; + private Disposable disposable; + private boolean isJumpToFeed; + private DownloadResult downloadResult; + private Feed feed = null; + private String podcastName = null; + private String episodeName = null; + private String url = "unknown"; + private String clipboardContent = ""; - public DownloadLogDetailsDialog(@NonNull Context context, DownloadResult status) { - super(context); - - String url = "unknown"; - if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); - if (media != null) { - url = media.getDownloadUrl(); - } - } else if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - Feed feed = DBReader.getFeed(status.getFeedfileId(), false, 0, 0); - if (feed != null) { - url = feed.getDownloadUrl(); - } - } - - String message = context.getString(R.string.download_successful); - if (!status.isSuccessful()) { - message = status.getReasonDetailed(); - } - - String humanReadableReason = context.getString(DownloadErrorLabel.from(status.getReason())); - SpannableString errorMessage = new SpannableString(context.getString(R.string.download_log_details_message, - humanReadableReason, message, url)); - errorMessage.setSpan(new ForegroundColorSpan(0x88888888), - humanReadableReason.length(), errorMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - - setTitle(R.string.download_error_details); - setMessage(errorMessage); - setPositiveButton(android.R.string.ok, null); - setNeutralButton(R.string.copy_to_clipboard, (dialog, which) -> { - ClipboardManager clipboard = (ClipboardManager) getContext() - .getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(context.getString(R.string.download_error_details), errorMessage); - clipboard.setPrimaryClip(clip); - if (Build.VERSION.SDK_INT < 32) { - EventBus.getDefault().post(new MessageEvent(context.getString(R.string.copied_to_clipboard))); - } - }); + public static DownloadLogDetailsDialog newInstance(DownloadResult downloadResult, boolean isJumpToFeed) { + DownloadLogDetailsDialog dialog = new DownloadLogDetailsDialog(); + Bundle args = new Bundle(); + args.putSerializable(EXTRA_DOWNLOAD_RESULT, downloadResult); + args.putBoolean(EXTRA_IS_JUMP_TO_FEED, isJumpToFeed); + dialog.setArguments(args); + return dialog; } @Override - public AlertDialog show() { - AlertDialog dialog = super.show(); - ((TextView) dialog.findViewById(android.R.id.message)).setTextIsSelectable(true); - return dialog; + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + downloadResult = (DownloadResult) getArguments().getSerializable(EXTRA_DOWNLOAD_RESULT); + isJumpToFeed = getArguments().getBoolean(EXTRA_IS_JUMP_TO_FEED, true); } -} + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(getContext()); + dialog.setTitle(R.string.download_error_details); + dialog.setPositiveButton(android.R.string.ok, null); + dialog.setNeutralButton(R.string.copy_to_clipboard, (copyDialog, which) -> + ClipboardUtils.copyText(viewBinding.getRoot(), R.string.download_error_details, clipboardContent)); + + viewBinding = DownloadLogDetailsDialogBinding.inflate(getLayoutInflater()); + dialog.setView(viewBinding.getRoot()); + + viewBinding.goToPodcastButton.setVisibility(View.GONE); + viewBinding.goToPodcastButton.setOnClickListener(v -> { + goToFeed(); + dismiss(); + Fragment downloadLog = getParentFragmentManager().findFragmentByTag(DownloadLogFragment.TAG); + if (downloadLog instanceof DownloadLogFragment) { + ((DownloadLogFragment) downloadLog).dismiss(); + } + }); + viewBinding.fileUrlLabel.setOnClickListener(v -> + ClipboardUtils.copyText(viewBinding.fileUrlLabel, R.string.download_log_details_file_url_title)); + viewBinding.technicalReasonLabel.setOnClickListener(v -> + ClipboardUtils.copyText(viewBinding.technicalReasonLabel, + R.string.download_log_details_technical_reason_title)); + + loadData(); + return dialog.create(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposable != null) { + disposable.dispose(); + } + } + + private void loadData() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Single.create(emitter -> { + if (downloadResult.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + FeedMedia media = DBReader.getFeedMedia(downloadResult.getFeedfileId()); + if (media != null) { + if (media.getItem() != null && media.getItem().getFeed() != null) { + feed = media.getItem().getFeed(); + podcastName = feed.getTitle(); + } + episodeName = media.getEpisodeTitle(); + url = media.getDownloadUrl(); + } else { + episodeName = downloadResult.getTitle(); + } + } else if (downloadResult.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + feed = DBReader.getFeed(downloadResult.getFeedfileId(), false, 0, 0); + if (feed != null) { + podcastName = feed.getTitle(); + url = feed.getDownloadUrl(); + } else { + podcastName = downloadResult.getTitle(); + } + } + emitter.onSuccess(true); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(obj -> updateUi(), + error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + private void updateUi() { + String message = getString(R.string.download_successful); + if (!downloadResult.isSuccessful()) { + message = downloadResult.getReasonDetailed(); + } + viewBinding.goToPodcastButton.setVisibility((isJumpToFeed && feed != null) ? View.VISIBLE : View.GONE); + viewBinding.podcastNameLabel.setText(podcastName); + viewBinding.podcastContainer.setVisibility(podcastName == null ? View.GONE : View.VISIBLE); + viewBinding.episodeNameLabel.setText(episodeName); + viewBinding.episodeContainer.setVisibility(episodeName == null ? View.GONE : View.VISIBLE); + + final String humanReadableReason = getString(DownloadErrorLabel.from(downloadResult.getReason())); + viewBinding.humanReadableReasonLabel.setText(humanReadableReason); + viewBinding.technicalReasonLabel.setText(message); + viewBinding.fileUrlLabel.setText(url); + + final String humanReadableReasonTitle = getString(R.string.download_log_details_human_readable_reason_title); + final String technicalReasonTitle = getString(R.string.download_log_details_technical_reason_title); + final String urlTitle = getString(R.string.download_log_details_file_url_title); + clipboardContent = String.format("%s: \n%s \n\n%s: \n%s \n\n%s: \n%s", + humanReadableReasonTitle, humanReadableReason, technicalReasonTitle, message, urlTitle, url); + } + + void goToFeed() { + if (feed == null) { + return; + } + Intent intent; + if (feed.getState() == Feed.STATE_SUBSCRIBED) { + intent = new MainActivityStarter(getContext()).withOpenFeed(feed.getId()).getIntent(); + } else { + intent = new OnlineFeedviewActivityStarter(getContext(), feed.getDownloadUrl()).getIntent(); + } + getContext().startActivity(intent); + } +} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogFragment.java index 993018a33..e7ea74db4 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogFragment.java @@ -12,14 +12,14 @@ import androidx.annotation.Nullable; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.DownloadLogFragmentBinding; import de.danoeh.antennapod.event.DownloadLogEvent; +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.databinding.DownloadLogFragmentBinding; -import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.ui.view.EmptyViewHandler; -import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; import org.greenrobot.eventbus.EventBus; @@ -33,7 +33,7 @@ import java.util.List; */ public class DownloadLogFragment extends BottomSheetDialogFragment implements AdapterView.OnItemClickListener, MaterialToolbar.OnMenuItemClickListener { - private static final String TAG = "DownloadLogFragment"; + public static final String TAG = "DownloadLogFragment"; private List downloadLog = new ArrayList<>(); private DownloadLogAdapter adapter; @@ -86,7 +86,8 @@ public class DownloadLogFragment extends BottomSheetDialogFragment public void onItemClick(AdapterView parent, View view, int position, long id) { final DownloadResult item = adapter.getItem(position); if (item != null) { - new DownloadLogDetailsDialog(getContext(), item).show(); + DownloadLogDetailsDialog.newInstance(item, true) + .show(getParentFragmentManager(), DownloadLogDetailsDialog.TAG); } } diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemlistFragment.java index 922a60a3a..ef1ab25d8 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemlistFragment.java @@ -624,9 +624,10 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - downloadStatus -> new DownloadLogDetailsDialog(getContext(), downloadStatus).show(), + downloadStatus -> DownloadLogDetailsDialog.newInstance(downloadStatus, false) + .show(getChildFragmentManager(), DownloadLogDetailsDialog.TAG), error -> error.printStackTrace(), - () -> new DownloadLogFragment().show(getChildFragmentManager(), null)); + () -> new DownloadLogFragment().show(getChildFragmentManager(), DownloadLogFragment.TAG)); } private void showFeedInfo() { diff --git a/app/src/main/res/layout/download_log_details_dialog.xml b/app/src/main/res/layout/download_log_details_dialog.xml new file mode 100644 index 000000000..1f6399ffe --- /dev/null +++ b/app/src/main/res/layout/download_log_details_dialog.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/src/main/java/de/danoeh/antennapod/model/download/DownloadResult.java b/model/src/main/java/de/danoeh/antennapod/model/download/DownloadResult.java index 8654db87e..2d8d0ae1c 100644 --- a/model/src/main/java/de/danoeh/antennapod/model/download/DownloadResult.java +++ b/model/src/main/java/de/danoeh/antennapod/model/download/DownloadResult.java @@ -2,12 +2,15 @@ package de.danoeh.antennapod.model.download; import androidx.annotation.NonNull; +import java.io.Serializable; import java.util.Date; /** * Contains status attributes for one download */ -public class DownloadResult { +public class DownloadResult implements Serializable { + private static final long serialVersionUID = 1L; + /** * Downloaders should use this constant for the size attribute if necessary * so that the listadapters etc. can react properly. diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml index 57bc355de..e27659c8f 100644 --- a/ui/i18n/src/main/res/values/strings.xml +++ b/ui/i18n/src/main/res/values/strings.xml @@ -304,6 +304,10 @@ Download running Details %1$s \n\nTechnical reason: \n%2$s \n\nFile URL:\n%3$s + Status + Technical details + File URL + Open Download of \"%1$s\" failed. Will be retried later. Download of \"%1$s\" failed. Tap to view details. @@ -387,7 +391,9 @@ Keep sorted Date Duration + Episode Episode title + Podcast Podcast title Random Smart shuffle