package de.danoeh.antennapod.fragment; import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.support.v4.view.GestureDetectorCompat; import android.text.Layout; import android.text.TextUtils; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.bumptech.glide.Glide; import com.joanzapata.iconify.Iconify; import com.joanzapata.iconify.widget.IconButton; import org.apache.commons.lang3.ArrayUtils; import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.CastEnabledActivity; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.event.FavoritesEvent; import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ShareUtils; import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.view.OnSwipeGesture; import de.danoeh.antennapod.view.SwipeGestureDetector; import de.greenrobot.event.EventBus; import rx.Observable; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; /** * Displays information about a FeedItem and actions. */ public class ItemFragment extends Fragment implements OnSwipeGesture { private static final String TAG = "ItemFragment"; private static final int EVENTS = EventDistributor.UNREAD_ITEMS_UPDATE; private static final String ARG_FEEDITEMS = "feeditems"; private static final String ARG_FEEDITEM_POS = "feeditem_pos"; private GestureDetectorCompat headerGestureDetector; private GestureDetectorCompat webviewGestureDetector; /** * Creates a new instance of an ItemFragment * * @param feeditem The ID of the FeedItem that should be displayed. * @return The ItemFragment instance */ public static ItemFragment newInstance(long feeditem) { return newInstance(new long[] { feeditem }, 0); } /** * Creates a new instance of an ItemFragment * * @param feeditems The IDs of the FeedItems that belong to the same list * @param feedItemPos The position of the FeedItem that is currently shown * @return The ItemFragment instance */ public static ItemFragment newInstance(long[] feeditems, int feedItemPos) { ItemFragment fragment = new ItemFragment(); Bundle args = new Bundle(); args.putLongArray(ARG_FEEDITEMS, feeditems); args.putInt(ARG_FEEDITEM_POS, feedItemPos); fragment.setArguments(args); return fragment; } private boolean itemsLoaded = false; private long[] feedItems; private int feedItemPos; private FeedItem item; private String webviewData; private List<Downloader> downloaderList; private ViewGroup root; private WebView webvDescription; private TextView txtvPodcast; private TextView txtvTitle; private TextView txtvDuration; private TextView txtvPublished; private ImageView imgvCover; private ProgressBar progbarDownload; private ProgressBar progbarLoading; private IconButton butAction1; private IconButton butAction2; private Menu popupMenu; private Subscription subscription; /** * URL that was selected via long-press. */ private String selectedURL; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); setHasOptionsMenu(true); feedItems = getArguments().getLongArray(ARG_FEEDITEMS); feedItemPos = getArguments().getInt(ARG_FEEDITEM_POS); headerGestureDetector = new GestureDetectorCompat(getActivity(), new SwipeGestureDetector(this)); webviewGestureDetector = new GestureDetectorCompat(getActivity(), new SwipeGestureDetector(this) { // necessary for the longclick context menu to work properly @Override public boolean onDown(MotionEvent e) { return false; } }); } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); View layout = inflater.inflate(R.layout.feeditem_fragment, container, false); root = (ViewGroup) layout.findViewById(R.id.content_root); LinearLayout header = (LinearLayout) root.findViewById(R.id.header); if(feedItems.length > 0) { header.setOnTouchListener((v, event) -> headerGestureDetector.onTouchEvent(event)); } txtvPodcast = (TextView) layout.findViewById(R.id.txtvPodcast); txtvPodcast.setOnClickListener(v -> openPodcast()); txtvTitle = (TextView) layout.findViewById(R.id.txtvTitle); if(Build.VERSION.SDK_INT >= 23) { txtvTitle.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); } txtvDuration = (TextView) layout.findViewById(R.id.txtvDuration); txtvPublished = (TextView) layout.findViewById(R.id.txtvPublished); if (Build.VERSION.SDK_INT >= 14) { // ellipsize is causing problems on old versions, see #448 txtvTitle.setEllipsize(TextUtils.TruncateAt.END); } webvDescription = (WebView) layout.findViewById(R.id.webvDescription); if (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) { if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { webvDescription.setLayerType(View.LAYER_TYPE_SOFTWARE, null); } webvDescription.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.black)); } webvDescription.getSettings().setUseWideViewPort(false); webvDescription.getSettings().setLayoutAlgorithm( WebSettings.LayoutAlgorithm.NARROW_COLUMNS); webvDescription.getSettings().setLoadWithOverviewMode(true); if(feedItems.length > 0) { webvDescription.setOnLongClickListener(webViewLongClickListener); } webvDescription.setOnTouchListener((v, event) -> webviewGestureDetector.onTouchEvent(event)); webvDescription.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); if(IntentUtils.isCallable(getActivity(), intent)) { startActivity(intent); } return true; } }); registerForContextMenu(webvDescription); imgvCover = (ImageView) layout.findViewById(R.id.imgvCover); imgvCover.setOnClickListener(v -> openPodcast()); progbarDownload = (ProgressBar) layout.findViewById(R.id.progbarDownload); progbarLoading = (ProgressBar) layout.findViewById(R.id.progbarLoading); butAction1 = (IconButton) layout.findViewById(R.id.butAction1); butAction2 = (IconButton) layout.findViewById(R.id.butAction2); butAction1.setOnClickListener(v -> { if (item == null) { return; } DefaultActionButtonCallback actionButtonCallback = new DefaultActionButtonCallback(getActivity()); actionButtonCallback.onActionButtonPressed(item); FeedMedia media = item.getMedia(); if (media != null && media.isDownloaded()) { // playback was started, dialog should close itself ((MainActivity) getActivity()).dismissChildFragment(); } }); butAction2.setOnClickListener(v -> { if (item == null) { return; } if (item.hasMedia()) { FeedMedia media = item.getMedia(); if (!media.isDownloaded()) { DBTasks.playMedia(getActivity(), media, true, true, true); ((MainActivity) getActivity()).dismissChildFragment(); } else { DBWriter.deleteFeedMediaOfItem(getActivity(), media.getId()); } } else if (item.getLink() != null) { Uri uri = Uri.parse(item.getLink()); getActivity().startActivity(new Intent(Intent.ACTION_VIEW, uri)); } }); return layout; } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); load(); } @Override public void onResume() { super.onResume(); EventDistributor.getInstance().register(contentUpdate); EventBus.getDefault().registerSticky(this); if(itemsLoaded) { progbarLoading.setVisibility(View.GONE); updateAppearance(); } } @Override public void onPause() { super.onPause(); EventDistributor.getInstance().unregister(contentUpdate); EventBus.getDefault().unregister(this); } @Override public void onDestroyView() { super.onDestroyView(); if(subscription != null) { subscription.unsubscribe(); } if (webvDescription != null && root != null) { root.removeView(webvDescription); webvDescription.destroy(); } } @Override public boolean onSwipeLeftToRight() { Log.d(TAG, "onSwipeLeftToRight()"); feedItemPos = feedItemPos - 1; if(feedItemPos < 0) { feedItemPos = feedItems.length - 1; } load(); return true; } @Override public boolean onSwipeRightToLeft() { Log.d(TAG, "onSwipeRightToLeft()"); feedItemPos = (feedItemPos + 1) % feedItems.length; load(); return true; } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if(!isAdded() || item == null) { return; } ((CastEnabledActivity) getActivity()).requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS); inflater.inflate(R.menu.feeditem_options, menu); popupMenu = menu; if (item.hasMedia()) { FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, null); } else { // these are already available via button1 and button2 FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, null, R.id.mark_read_item, R.id.visit_website_item); } } @Override public boolean onOptionsItemSelected(MenuItem menuItem) { switch(menuItem.getItemId()) { case R.id.open_podcast: openPodcast(); return true; default: try { return FeedItemMenuHandler.onMenuItemClicked(getActivity(), menuItem.getItemId(), item); } catch (DownloadRequestException e) { e.printStackTrace(); Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show(); return true; } } } private final FeedItemMenuHandler.MenuInterface popupMenuInterface = new FeedItemMenuHandler.MenuInterface() { @Override public void setItemVisibility(int id, boolean visible) { MenuItem item = popupMenu.findItem(id); if (item != null) { item.setVisible(visible); } } }; private void onFragmentLoaded() { if (webviewData != null) { webvDescription.loadDataWithBaseURL(null, webviewData, "text/html", "utf-8", "about:blank"); } updateAppearance(); } private void updateAppearance() { if (item == null) { Log.d(TAG, "updateAppearance item is null"); return; } getActivity().supportInvalidateOptionsMenu(); txtvPodcast.setText(item.getFeed().getTitle()); txtvTitle.setText(item.getTitle()); if (item.getPubDate() != null) { String pubDateStr = DateUtils.formatAbbrev(getActivity(), item.getPubDate()); txtvPublished.setText(pubDateStr); } Glide.with(getActivity()) .load(item.getImageUri()) .placeholder(R.color.light_gray) .error(R.color.light_gray) .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) .fitCenter() .dontAnimate() .into(imgvCover); progbarDownload.setVisibility(View.GONE); if (item.hasMedia() && downloaderList != null) { for (Downloader downloader : downloaderList) { if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { progbarDownload.setVisibility(View.VISIBLE); progbarDownload.setProgress(downloader.getDownloadRequest().getProgressPercent()); } } } FeedMedia media = item.getMedia(); String butAction1Icon = null; int butAction1Text = 0; String butAction2Icon = null; int butAction2Text = 0; if (media == null) { if (!item.isPlayed()) { butAction1Icon = "{fa-check 24sp}"; butAction1Text = R.string.mark_read_label; } if (item.getLink() != null) { butAction2Icon = "{md-web 24sp}"; butAction2Text = R.string.visit_website_label; } } else { if(media.getDuration() > 0) { txtvDuration.setText(Converter.getDurationStringLong(media.getDuration())); } boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media); if (!media.isDownloaded()) { butAction2Icon = "{md-settings-input-antenna 24sp}"; butAction2Text = R.string.stream_label; } else { butAction2Icon = "{md-delete 24sp}"; butAction2Text = R.string.remove_label; } if (isDownloading) { butAction1Icon = "{md-cancel 24sp}"; butAction1Text = R.string.cancel_label; } else if (media.isDownloaded()) { butAction1Icon = "{md-play-arrow 24sp}"; butAction1Text = R.string.play_label; } else { butAction1Icon = "{md-file-download 24sp}"; butAction1Text = R.string.download_label; } } if(butAction1Icon != null && butAction1Text != 0) { butAction1.setText(butAction1Icon +"\u0020\u0020" + getActivity().getString(butAction1Text)); Iconify.addIcons(butAction1); butAction1.setVisibility(View.VISIBLE); } else { butAction1.setVisibility(View.INVISIBLE); } if(butAction2Icon != null && butAction2Text != 0) { butAction2.setText(butAction2Icon +"\u0020\u0020" + getActivity().getString(butAction2Text)); Iconify.addIcons(butAction2); butAction2.setVisibility(View.VISIBLE); } else { butAction2.setVisibility(View.INVISIBLE); } } private View.OnLongClickListener webViewLongClickListener = new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { WebView.HitTestResult r = webvDescription.getHitTestResult(); if (r != null && r.getType() == WebView.HitTestResult.SRC_ANCHOR_TYPE) { Log.d(TAG, "Link of webview was long-pressed. Extra: " + r.getExtra()); selectedURL = r.getExtra(); webvDescription.showContextMenu(); return true; } selectedURL = null; return false; } }; @Override public boolean onContextItemSelected(MenuItem item) { boolean handled = selectedURL != null; if (selectedURL != null) { switch (item.getItemId()) { case R.id.open_in_browser_item: Uri uri = Uri.parse(selectedURL); final Intent intent = new Intent(Intent.ACTION_VIEW, uri); if(IntentUtils.isCallable(getActivity(), intent)) { getActivity().startActivity(intent); } break; case R.id.share_url_item: ShareUtils.shareLink(getActivity(), selectedURL); break; case R.id.copy_url_item: if (android.os.Build.VERSION.SDK_INT >= 11) { ClipData clipData = ClipData.newPlainText(selectedURL, selectedURL); android.content.ClipboardManager cm = (android.content.ClipboardManager) getActivity() .getSystemService(Context.CLIPBOARD_SERVICE); cm.setPrimaryClip(clipData); } else { android.text.ClipboardManager cm = (android.text.ClipboardManager) getActivity() .getSystemService(Context.CLIPBOARD_SERVICE); cm.setText(selectedURL); } Toast t = Toast.makeText(getActivity(), R.string.copied_url_msg, Toast.LENGTH_SHORT); t.show(); break; default: handled = false; break; } selectedURL = null; } return handled; } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { if (selectedURL != null) { super.onCreateContextMenu(menu, v, menuInfo); Uri uri = Uri.parse(selectedURL); final Intent intent = new Intent(Intent.ACTION_VIEW, uri); if(IntentUtils.isCallable(getActivity(), intent)) { menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE, R.string.open_in_browser_label); } menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE, R.string.copy_url_label); menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE, R.string.share_url_label); menu.setHeaderTitle(selectedURL); } } private void openPodcast() { Fragment fragment = ItemlistFragment.newInstance(item.getFeedId()); ((MainActivity)getActivity()).loadChildFragment(fragment); } public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); for(FeedItem item : event.items) { if(feedItems[feedItemPos] == item.getId()) { load(); return; } } } public void onEventMainThread(DownloadEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; downloaderList = update.downloaders; if(item == null || item.getMedia() == null) { return; } long mediaId = item.getMedia().getId(); if(ArrayUtils.contains(update.mediaIds, mediaId)) { if (itemsLoaded && getActivity() != null) { updateAppearance(); } } } private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { @Override public void update(EventDistributor eventDistributor, Integer arg) { if ((arg & EVENTS) != 0) { load(); } } }; private void load() { if(subscription != null) { subscription.unsubscribe(); } progbarLoading.setVisibility(View.VISIBLE); subscription = Observable.fromCallable(this::loadInBackground) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { progbarLoading.setVisibility(View.GONE); item = result; itemsLoaded = true; onFragmentLoaded(); }, error -> { Log.e(TAG, Log.getStackTraceString(error)); }); } private FeedItem loadInBackground() { FeedItem feedItem = DBReader.getFeedItem(feedItems[feedItemPos]); if (feedItem != null) { Timeline t = new Timeline(getActivity(), feedItem); webviewData = t.processShownotes(false); } return feedItem; } }