package de.danoeh.antennapod.ui.screen.feed; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.graphics.LightingColorFilter; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; import androidx.documentfile.provider.DocumentFile; import androidx.fragment.app.Fragment; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.CollapsingToolbarLayout; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.ui.TransitionEffect; import de.danoeh.antennapod.storage.database.DBReader; import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; import de.danoeh.antennapod.ui.common.IntentUtils; import de.danoeh.antennapod.ui.share.ShareUtils; import de.danoeh.antennapod.ui.cleaner.HtmlToPlainText; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedFunding; import de.danoeh.antennapod.ui.glide.FastBlurTransformation; import de.danoeh.antennapod.ui.screen.feed.preferences.EditUrlSettingsDialog; import de.danoeh.antennapod.ui.statistics.StatisticsFragment; import de.danoeh.antennapod.ui.statistics.feed.FeedStatisticsFragment; import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.MaybeOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.Iterator; /** * Displays information about a feed. */ public class FeedInfoFragment extends Fragment implements MaterialToolbar.OnMenuItemClickListener { private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId"; private static final String TAG = "FeedInfoActivity"; private final ActivityResultLauncher addLocalFolderLauncher = registerForActivityResult(new AddLocalFolder(), this::addLocalFolderResult); private Feed feed; private Disposable disposable; private ImageView imgvCover; private TextView txtvTitle; private TextView txtvDescription; private TextView txtvFundingUrl; private TextView lblSupport; private TextView txtvUrl; private TextView txtvAuthorHeader; private ImageView imgvBackground; private View infoContainer; private View header; private MaterialToolbar toolbar; public static FeedInfoFragment newInstance(Feed feed) { FeedInfoFragment fragment = new FeedInfoFragment(); Bundle arguments = new Bundle(); arguments.putLong(EXTRA_FEED_ID, feed.getId()); fragment.setArguments(arguments); return fragment; } private final View.OnClickListener copyUrlToClipboard = new View.OnClickListener() { @Override public void onClick(View v) { if (feed != null && feed.getDownloadUrl() != null) { String url = feed.getDownloadUrl(); ClipData clipData = ClipData.newPlainText(url, url); android.content.ClipboardManager cm = (android.content.ClipboardManager) getContext() .getSystemService(Context.CLIPBOARD_SERVICE); cm.setPrimaryClip(clipData); if (Build.VERSION.SDK_INT <= 32) { ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT); } } } }; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.feedinfo, null); toolbar = root.findViewById(R.id.toolbar); toolbar.setTitle(""); toolbar.inflateMenu(R.menu.feedinfo); toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); toolbar.setOnMenuItemClickListener(this); refreshToolbarState(); AppBarLayout appBar = root.findViewById(R.id.appBar); CollapsingToolbarLayout collapsingToolbar = root.findViewById(R.id.collapsing_toolbar); ToolbarIconTintManager iconTintManager = new ToolbarIconTintManager(getContext(), toolbar, collapsingToolbar) { @Override protected void doTint(Context themedContext) { toolbar.getMenu().findItem(R.id.visit_website_item) .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_web)); toolbar.getMenu().findItem(R.id.share_item) .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_share)); } }; iconTintManager.updateTint(); appBar.addOnOffsetChangedListener(iconTintManager); imgvCover = root.findViewById(R.id.imgvCover); txtvTitle = root.findViewById(R.id.txtvTitle); txtvAuthorHeader = root.findViewById(R.id.txtvAuthor); imgvBackground = root.findViewById(R.id.imgvBackground); header = root.findViewById(R.id.headerContainer); infoContainer = root.findViewById(R.id.infoContainer); root.findViewById(R.id.butShowInfo).setVisibility(View.INVISIBLE); root.findViewById(R.id.butShowSettings).setVisibility(View.INVISIBLE); root.findViewById(R.id.butFilter).setVisibility(View.INVISIBLE); // https://github.com/bumptech/glide/issues/529 imgvBackground.setColorFilter(new LightingColorFilter(0xff828282, 0x000000)); txtvDescription = root.findViewById(R.id.txtvDescription); txtvUrl = root.findViewById(R.id.txtvUrl); lblSupport = root.findViewById(R.id.lblSupport); txtvFundingUrl = root.findViewById(R.id.txtvFundingUrl); txtvUrl.setOnClickListener(copyUrlToClipboard); long feedId = getArguments().getLong(EXTRA_FEED_ID); getParentFragmentManager().beginTransaction().replace(R.id.statisticsFragmentContainer, FeedStatisticsFragment.newInstance(feedId, false), "feed_statistics_fragment") .commitAllowingStateLoss(); root.findViewById(R.id.btnvOpenStatistics).setOnClickListener(view -> { StatisticsFragment fragment = new StatisticsFragment(); ((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE); }); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { long feedId = getArguments().getLong(EXTRA_FEED_ID); disposable = Maybe.create((MaybeOnSubscribe) emitter -> { Feed feed = DBReader.getFeed(feedId); if (feed != null) { emitter.onSuccess(feed); } else { emitter.onComplete(); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { feed = result; showFeed(); }, error -> Log.d(TAG, Log.getStackTraceString(error)), () -> { }); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); if (header == null || infoContainer == null) { return; } int horizontalSpacing = (int) getResources().getDimension(R.dimen.additional_horizontal_spacing); header.setPadding(horizontalSpacing, header.getPaddingTop(), horizontalSpacing, header.getPaddingBottom()); infoContainer.setPadding(horizontalSpacing, infoContainer.getPaddingTop(), horizontalSpacing, infoContainer.getPaddingBottom()); } private void showFeed() { Log.d(TAG, "Language is " + feed.getLanguage()); Log.d(TAG, "Author is " + feed.getAuthor()); Log.d(TAG, "URL is " + feed.getDownloadUrl()); Glide.with(this) .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.light_gray) .error(R.color.light_gray) .fitCenter() .dontAnimate()) .into(imgvCover); Glide.with(this) .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.image_readability_tint) .error(R.color.image_readability_tint) .transform(new FastBlurTransformation()) .dontAnimate()) .into(imgvBackground); txtvTitle.setText(feed.getTitle()); txtvTitle.setMaxLines(6); String description = HtmlToPlainText.getPlainText(feed.getDescription()); txtvDescription.setText(description); if (!TextUtils.isEmpty(feed.getAuthor())) { txtvAuthorHeader.setText(feed.getAuthor()); } txtvUrl.setText(feed.getDownloadUrl()); txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0); if (feed.getPaymentLinks() == null || feed.getPaymentLinks().size() == 0) { lblSupport.setVisibility(View.GONE); txtvFundingUrl.setVisibility(View.GONE); } else { lblSupport.setVisibility(View.VISIBLE); ArrayList fundingList = feed.getPaymentLinks(); // Filter for duplicates, but keep items in the order that they have in the feed. Iterator i = fundingList.iterator(); while (i.hasNext()) { FeedFunding funding = i.next(); for (FeedFunding other : fundingList) { if (TextUtils.equals(other.url, funding.url)) { if (other.content != null && funding.content != null && other.content.length() > funding.content.length()) { i.remove(); break; } } } } StringBuilder str = new StringBuilder(); for (FeedFunding funding : fundingList) { str.append(funding.content.isEmpty() ? getContext().getResources().getString(R.string.support_podcast) : funding.content).append(" ").append(funding.url); str.append("\n"); } str = new StringBuilder(StringUtils.trim(str.toString())); txtvFundingUrl.setText(str.toString()); } refreshToolbarState(); } @Override public void onDestroy() { super.onDestroy(); if (disposable != null) { disposable.dispose(); } } private void refreshToolbarState() { toolbar.getMenu().findItem(R.id.reconnect_local_folder).setVisible(feed != null && feed.isLocalFeed()); toolbar.getMenu().findItem(R.id.share_item).setVisible(feed != null && !feed.isLocalFeed()); toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null && IntentUtils.isCallable(getContext(), new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink())))); toolbar.getMenu().findItem(R.id.edit_feed_url_item).setVisible(feed != null && !feed.isLocalFeed()); } @Override public boolean onMenuItemClick(MenuItem item) { if (feed == null) { ((MainActivity) getActivity()).showSnackbarAbovePlayer( R.string.please_wait_for_data, Toast.LENGTH_LONG); return false; } if (item.getItemId() == R.id.visit_website_item) { IntentUtils.openInBrowser(getContext(), feed.getLink()); } else if (item.getItemId() == R.id.share_item) { ShareUtils.shareFeedLink(getContext(), feed); } else if (item.getItemId() == R.id.reconnect_local_folder) { MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(getContext()); alert.setMessage(R.string.reconnect_local_folder_warning); alert.setPositiveButton(android.R.string.ok, (dialog, which) -> { try { addLocalFolderLauncher.launch(null); } catch (ActivityNotFoundException e) { Log.e(TAG, "No activity found. Should never happen..."); } }); alert.setNegativeButton(android.R.string.cancel, null); alert.show(); } else if (item.getItemId() == R.id.edit_feed_url_item) { new EditUrlSettingsDialog(getActivity(), feed) { @Override protected void setUrl(String url) { feed.setDownloadUrl(url); txtvUrl.setText(feed.getDownloadUrl()); txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0); } }.show(); } else { return false; } return true; } private void addLocalFolderResult(final Uri uri) { if (uri == null) { return; } reconnectLocalFolder(uri); } private void reconnectLocalFolder(Uri uri) { if (feed == null) { return; } Completable.fromAction(() -> { getActivity().getContentResolver() .takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), uri); if (documentFile == null) { throw new IllegalArgumentException("Unable to retrieve document tree"); } feed.setDownloadUrl(Feed.PREFIX_LOCAL_FOLDER + uri.toString()); FeedDatabaseWriter.updateFeed(getContext(), feed, true); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( () -> ((MainActivity) getActivity()) .showSnackbarAbovePlayer(android.R.string.ok, Snackbar.LENGTH_SHORT), error -> ((MainActivity) getActivity()) .showSnackbarAbovePlayer(error.getLocalizedMessage(), Snackbar.LENGTH_LONG)); } private static class AddLocalFolder extends ActivityResultContracts.OpenDocumentTree { @NonNull @Override public Intent createIntent(@NonNull final Context context, @Nullable final Uri input) { return super.createIntent(context, input) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } } }