mirror of
https://github.com/viq/NewsBlur.git
synced 2025-09-18 21:43:31 +00:00
916 lines
No EOL
40 KiB
Java
916 lines
No EOL
40 KiB
Java
package com.newsblur.fragment;
|
|
|
|
import android.app.Activity;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.DialogInterface;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.content.res.Configuration;
|
|
import android.graphics.Color;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.graphics.drawable.GradientDrawable;
|
|
import android.net.Uri;
|
|
import android.os.AsyncTask;
|
|
import android.os.Bundle;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.appcompat.app.AlertDialog;
|
|
import androidx.appcompat.widget.PopupMenu;
|
|
import androidx.fragment.app.DialogFragment;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.view.ContextMenu;
|
|
import android.view.LayoutInflater;
|
|
import android.view.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.View;
|
|
import android.view.View.OnClickListener;
|
|
import android.view.ViewGroup;
|
|
import android.webkit.WebView.HitTestResult;
|
|
import android.widget.ImageView;
|
|
import android.widget.TextView;
|
|
|
|
import com.newsblur.R;
|
|
import com.newsblur.activity.FeedItemsList;
|
|
import com.newsblur.activity.NbActivity;
|
|
import com.newsblur.activity.Reading;
|
|
import com.newsblur.databinding.FragmentReadingitemBinding;
|
|
import com.newsblur.databinding.IncludeReadingItemCommentBinding;
|
|
import com.newsblur.domain.Classifier;
|
|
import com.newsblur.domain.Story;
|
|
import com.newsblur.domain.UserDetails;
|
|
import com.newsblur.service.OriginalTextService;
|
|
import com.newsblur.util.DefaultFeedView;
|
|
import com.newsblur.util.FeedSet;
|
|
import com.newsblur.util.FeedUtils;
|
|
import com.newsblur.util.Font;
|
|
import com.newsblur.util.PrefConstants.ThemeValue;
|
|
import com.newsblur.util.PrefsUtils;
|
|
import com.newsblur.util.StoryUtils;
|
|
import com.newsblur.util.UIUtils;
|
|
import com.newsblur.view.ReadingScrollView;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
public class ReadingItemFragment extends NbFragment implements PopupMenu.OnMenuItemClickListener {
|
|
|
|
private static final String BUNDLE_SCROLL_POS_REL = "scrollStateRel";
|
|
public static final String TEXT_SIZE_CHANGED = "textSizeChanged";
|
|
public static final String TEXT_SIZE_VALUE = "textSizeChangeValue";
|
|
public static final String READING_FONT_CHANGED = "readingFontChanged";
|
|
public Story story;
|
|
private FeedSet fs;
|
|
private LayoutInflater inflater;
|
|
private String feedColor, feedTitle, feedFade, feedBorder, feedIconUrl, faviconText;
|
|
private Classifier classifier;
|
|
private BroadcastReceiver textSizeReceiver, readingFontReceiver;
|
|
private boolean displayFeedDetails;
|
|
private View view;
|
|
private UserDetails user;
|
|
private DefaultFeedView selectedFeedView;
|
|
private boolean textViewUnavailable;
|
|
|
|
/** The story HTML, as provided by the 'content' element of the stories API. */
|
|
private String storyContent;
|
|
/** The text-mode story HTML, as retrived via the secondary original text API. */
|
|
private String originalText;
|
|
|
|
private HashMap<String,String> imageAltTexts;
|
|
private HashMap<String,String> imageUrlRemaps;
|
|
private String sourceUserId;
|
|
private int contentHash;
|
|
|
|
// these three flags are progressively set by async callbacks and unioned
|
|
// to set isLoadFinished, when we trigger any final UI tricks.
|
|
private boolean isContentLoadFinished;
|
|
private boolean isWebLoadFinished;
|
|
private boolean isSocialLoadFinished;
|
|
private Boolean isLoadFinished = false;
|
|
private float savedScrollPosRel = 0f;
|
|
|
|
private final Object WEBVIEW_CONTENT_MUTEX = new Object();
|
|
|
|
private FragmentReadingitemBinding binding;
|
|
private IncludeReadingItemCommentBinding itemCommentBinding;
|
|
|
|
public static ReadingItemFragment newInstance(Story story, String feedTitle, String feedFaviconColor, String feedFaviconFade, String feedFaviconBorder, String faviconText, String faviconUrl, Classifier classifier, boolean displayFeedDetails, String sourceUserId) {
|
|
ReadingItemFragment readingFragment = new ReadingItemFragment();
|
|
|
|
Bundle args = new Bundle();
|
|
args.putSerializable("story", story);
|
|
args.putString("feedTitle", feedTitle);
|
|
args.putString("feedColor", feedFaviconColor);
|
|
args.putString("feedFade", feedFaviconFade);
|
|
args.putString("feedBorder", feedFaviconBorder);
|
|
args.putString("faviconText", faviconText);
|
|
args.putString("faviconUrl", faviconUrl);
|
|
args.putBoolean("displayFeedDetails", displayFeedDetails);
|
|
args.putSerializable("classifier", classifier);
|
|
args.putString("sourceUserId", sourceUserId);
|
|
readingFragment.setArguments(args);
|
|
|
|
return readingFragment;
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
story = getArguments() != null ? (Story) getArguments().getSerializable("story") : null;
|
|
|
|
displayFeedDetails = getArguments().getBoolean("displayFeedDetails");
|
|
|
|
user = PrefsUtils.getUserDetails(getActivity());
|
|
|
|
feedIconUrl = getArguments().getString("faviconUrl");
|
|
feedTitle = getArguments().getString("feedTitle");
|
|
feedColor = getArguments().getString("feedColor");
|
|
feedFade = getArguments().getString("feedFade");
|
|
feedBorder = getArguments().getString("feedBorder");
|
|
faviconText = getArguments().getString("faviconText");
|
|
|
|
classifier = (Classifier) getArguments().getSerializable("classifier");
|
|
|
|
sourceUserId = getArguments().getString("sourceUserId");
|
|
|
|
textSizeReceiver = new TextSizeReceiver();
|
|
getActivity().registerReceiver(textSizeReceiver, new IntentFilter(TEXT_SIZE_CHANGED));
|
|
readingFontReceiver = new ReadingFontReceiver();
|
|
getActivity().registerReceiver(readingFontReceiver, new IntentFilter(READING_FONT_CHANGED));
|
|
|
|
if (savedInstanceState != null) {
|
|
savedScrollPosRel = savedInstanceState.getFloat(BUNDLE_SCROLL_POS_REL);
|
|
// we can't actually use the saved scroll position until the webview finishes loading
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSaveInstanceState(Bundle outState) {
|
|
super.onSaveInstanceState(outState);
|
|
int heightm = binding.readingScrollview.getChildAt(0).getMeasuredHeight();
|
|
int pos = binding.readingScrollview.getScrollY();
|
|
outState.putFloat(BUNDLE_SCROLL_POS_REL, (((float)pos)/heightm));
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
getActivity().unregisterReceiver(textSizeReceiver);
|
|
getActivity().unregisterReceiver(readingFontReceiver);
|
|
binding.readingWebview.setOnTouchListener(null);
|
|
view.setOnTouchListener(null);
|
|
getActivity().getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(null);
|
|
super.onDestroy();
|
|
}
|
|
|
|
// WebViews don't automatically pause content like audio and video when they lose focus. Chain our own
|
|
// state into the webview so it behaves.
|
|
@Override
|
|
public void onPause() {
|
|
if (this.binding.readingWebview != null ) { this.binding.readingWebview.onPause(); }
|
|
super.onPause();
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
reloadStoryContent();
|
|
if (this.binding.readingWebview != null ) { this.binding.readingWebview.onResume(); }
|
|
}
|
|
|
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
this.inflater = inflater;
|
|
view = inflater.inflate(R.layout.fragment_readingitem, null);
|
|
binding = FragmentReadingitemBinding.bind(view);
|
|
itemCommentBinding = IncludeReadingItemCommentBinding.bind(binding.getRoot());
|
|
|
|
Reading activity = (Reading) getActivity();
|
|
fs = activity.getFeedSet();
|
|
|
|
selectedFeedView = PrefsUtils.getDefaultViewModeForFeed(activity, story.feedId);
|
|
|
|
registerForContextMenu(binding.readingWebview);
|
|
binding.readingWebview.setCustomViewLayout(binding.customViewContainer);
|
|
binding.readingWebview.setWebviewWrapperLayout(binding.readingScrollview);
|
|
binding.readingWebview.setBackgroundColor(Color.TRANSPARENT);
|
|
binding.readingWebview.fragment = this;
|
|
binding.readingWebview.activity = activity;
|
|
|
|
setupItemMetadata();
|
|
updateShareButton();
|
|
updateSaveButton();
|
|
setupItemCommentsAndShares();
|
|
|
|
ReadingScrollView scrollView = (ReadingScrollView) view.findViewById(R.id.reading_scrollview);
|
|
scrollView.registerScrollChangeListener(activity);
|
|
|
|
return view;
|
|
}
|
|
|
|
@Override
|
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
super.onViewCreated(view, savedInstanceState);
|
|
binding.storyContextMenuButton.setOnClickListener(new OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
onClickMenuButton();
|
|
}
|
|
});
|
|
itemCommentBinding.saveStoryButton.setOnClickListener(new OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
clickSave();
|
|
}
|
|
});
|
|
itemCommentBinding.shareStoryButton.setOnClickListener(new OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
clickShare();
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
|
HitTestResult result = binding.readingWebview.getHitTestResult();
|
|
if (result.getType() == HitTestResult.IMAGE_TYPE ||
|
|
result.getType() == HitTestResult.SRC_IMAGE_ANCHOR_TYPE ) {
|
|
// if the long-pressed item was an image, see if we can pop up a little dialogue
|
|
// that presents the alt text. Note that images wrapped in links tend to get detected
|
|
// as anchors, not images, and may not point to the corresponding image URL.
|
|
String imageURL = result.getExtra();
|
|
imageURL = imageURL.replace("file://", "");
|
|
String mappedURL = imageUrlRemaps.get(imageURL);
|
|
final String finalURL = mappedURL == null ? imageURL : mappedURL;
|
|
final String altText = imageAltTexts.get(finalURL);
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
|
builder.setTitle(finalURL);
|
|
if (altText != null) {
|
|
builder.setMessage(UIUtils.fromHtml(altText));
|
|
} else {
|
|
builder.setMessage(finalURL);
|
|
}
|
|
int actionRID = R.string.alert_dialog_openlink;
|
|
if (result.getType() == HitTestResult.IMAGE_TYPE || result.getType() == HitTestResult.SRC_IMAGE_ANCHOR_TYPE ) {
|
|
actionRID = R.string.alert_dialog_openimage;
|
|
}
|
|
builder.setPositiveButton(actionRID, new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog, int id) {
|
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
i.setData(Uri.parse(finalURL));
|
|
try {
|
|
startActivity(i);
|
|
} catch (Exception e) {
|
|
android.util.Log.wtf(this.getClass().getName(), "device cannot open URLs");
|
|
}
|
|
}
|
|
});
|
|
builder.setNegativeButton(R.string.alert_dialog_done, new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog, int id) {
|
|
; // do nothing
|
|
}
|
|
});
|
|
builder.show();
|
|
} else if (result.getType() == HitTestResult.SRC_ANCHOR_TYPE) {
|
|
String url = result.getExtra();
|
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
|
intent.setType("text/plain");
|
|
intent.putExtra(Intent.EXTRA_SUBJECT, UIUtils.fromHtml(story.title).toString());
|
|
intent.putExtra(Intent.EXTRA_TEXT, url);
|
|
startActivity(Intent.createChooser(intent, "Share using"));
|
|
} else {
|
|
super.onCreateContextMenu(menu, v, menuInfo);
|
|
}
|
|
}
|
|
|
|
private void onClickMenuButton() {
|
|
PopupMenu pm = new PopupMenu(getActivity(), binding.storyContextMenuButton);
|
|
Menu menu = pm.getMenu();
|
|
pm.getMenuInflater().inflate(R.menu.story_context, menu);
|
|
|
|
menu.findItem(R.id.menu_reading_save).setTitle(story.starred ? R.string.menu_unsave_story : R.string.menu_save_story);
|
|
if (fs.isFilterSaved() || fs.isAllSaved() || (fs.getSingleSavedTag() != null)) menu.findItem(R.id.menu_reading_markunread).setVisible(false);
|
|
|
|
ThemeValue themeValue = PrefsUtils.getSelectedTheme(getActivity());
|
|
if (themeValue == ThemeValue.LIGHT) {
|
|
menu.findItem(R.id.menu_theme_light).setChecked(true);
|
|
} else if (themeValue == ThemeValue.DARK) {
|
|
menu.findItem(R.id.menu_theme_dark).setChecked(true);
|
|
} else if (themeValue == ThemeValue.BLACK) {
|
|
menu.findItem(R.id.menu_theme_black).setChecked(true);
|
|
} else if (themeValue == ThemeValue.AUTO) {
|
|
menu.findItem(R.id.menu_theme_auto).setChecked(true);
|
|
}
|
|
|
|
pm.setOnMenuItemClickListener(this);
|
|
pm.show();
|
|
}
|
|
|
|
@Override
|
|
public boolean onMenuItemClick(MenuItem item) {
|
|
if (item.getItemId() == R.id.menu_reading_original) {
|
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
i.setData(Uri.parse(story.permalink));
|
|
try {
|
|
startActivity(i);
|
|
} catch (Exception e) {
|
|
com.newsblur.util.Log.e(this, "device cannot open URLs");
|
|
}
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_reading_sharenewsblur) {
|
|
String sourceUserId = null;
|
|
if (fs.getSingleSocialFeed() != null) sourceUserId = fs.getSingleSocialFeed().getKey();
|
|
DialogFragment newFragment = ShareDialogFragment.newInstance(story, sourceUserId);
|
|
newFragment.show(getActivity().getSupportFragmentManager(), "dialog");
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_send_story) {
|
|
FeedUtils.sendStoryBrief(story, getActivity());
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_send_story_full) {
|
|
FeedUtils.sendStoryFull(story, getActivity());
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_textsize) {
|
|
TextSizeDialogFragment textSize = TextSizeDialogFragment.newInstance(PrefsUtils.getTextSize(getActivity()), TextSizeDialogFragment.TextSizeType.ReadingText);
|
|
textSize.show(getActivity().getSupportFragmentManager(), TextSizeDialogFragment.class.getName());
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_font) {
|
|
ReadingFontDialogFragment storyFont = ReadingFontDialogFragment.newInstance(PrefsUtils.getFontString(getActivity()));
|
|
storyFont.show(getActivity().getSupportFragmentManager(), ReadingFontDialogFragment.class.getName());
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_reading_save) {
|
|
if (story.starred) {
|
|
FeedUtils.setStorySaved(story, false, getActivity(), null);
|
|
} else {
|
|
FeedUtils.setStorySaved(story.storyHash, true, getActivity());
|
|
}
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_reading_markunread) {
|
|
FeedUtils.markStoryUnread(story, getActivity());
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_theme_auto) {
|
|
PrefsUtils.setSelectedTheme(getActivity(), ThemeValue.AUTO);
|
|
UIUtils.restartActivity(getActivity());
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_theme_light) {
|
|
PrefsUtils.setSelectedTheme(getActivity(), ThemeValue.LIGHT);
|
|
UIUtils.restartActivity(getActivity());
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_theme_dark) {
|
|
PrefsUtils.setSelectedTheme(getActivity(), ThemeValue.DARK);
|
|
UIUtils.restartActivity(getActivity());
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_theme_black) {
|
|
PrefsUtils.setSelectedTheme(getActivity(), ThemeValue.BLACK);
|
|
UIUtils.restartActivity(getActivity());
|
|
return true;
|
|
} else if (item.getItemId() == R.id.menu_intel) {
|
|
if (story.feedId.equals("0")) return true; // cannot train on feedless stories
|
|
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
|
|
intelFrag.show(getActivity().getSupportFragmentManager(), StoryIntelTrainerFragment.class.getName());
|
|
return true;
|
|
} else if(item.getItemId() == R.id.menu_go_to_feed){
|
|
FeedItemsList.startActivity(getContext(), fs,
|
|
FeedUtils.getFeed(story.feedId), null);
|
|
return true;
|
|
} else {
|
|
return super.onOptionsItemSelected(item);
|
|
}
|
|
}
|
|
|
|
private void clickSave() {
|
|
if (story.starred) {
|
|
FeedUtils.setStorySaved(story.storyHash, false, getActivity());
|
|
} else {
|
|
FeedUtils.setStorySaved(story.storyHash,true, getActivity());
|
|
}
|
|
}
|
|
|
|
private void updateSaveButton() {
|
|
if (itemCommentBinding.saveStoryButton == null) return;
|
|
itemCommentBinding.saveStoryButton.setText(story.starred ? R.string.unsave_this : R.string.save_this);
|
|
}
|
|
|
|
private void clickShare() {
|
|
DialogFragment newFragment = ShareDialogFragment.newInstance(story, sourceUserId);
|
|
newFragment.show(getParentFragmentManager(), "dialog");
|
|
}
|
|
|
|
private void updateShareButton() {
|
|
if (itemCommentBinding.shareStoryButton == null) return;
|
|
for (String userId : story.sharedUserIds) {
|
|
if (TextUtils.equals(userId, user.id)) {
|
|
itemCommentBinding.shareStoryButton.setText(R.string.already_shared);
|
|
return;
|
|
}
|
|
}
|
|
itemCommentBinding.shareStoryButton.setText(R.string.share_this);
|
|
}
|
|
|
|
private void setupItemCommentsAndShares() {
|
|
new SetupCommentSectionTask(this, view, inflater, story).execute();
|
|
}
|
|
|
|
private void setupItemMetadata() {
|
|
View feedHeader = view.findViewById(R.id.row_item_feed_header);
|
|
View feedHeaderBorder = view.findViewById(R.id.item_feed_border);
|
|
TextView itemDate = (TextView) view.findViewById(R.id.reading_item_date);
|
|
ImageView feedIcon = (ImageView) view.findViewById(R.id.reading_feed_icon);
|
|
|
|
if ((feedColor == null) ||
|
|
(feedFade == null) ||
|
|
TextUtils.equals(feedColor, "null") ||
|
|
TextUtils.equals(feedFade, "null")) {
|
|
feedColor = "303030";
|
|
feedFade = "505050";
|
|
feedBorder = "202020";
|
|
}
|
|
|
|
int[] colors = {
|
|
Color.parseColor("#" + feedColor),
|
|
Color.parseColor("#" + feedFade),
|
|
};
|
|
GradientDrawable gradient = new GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, colors);
|
|
UIUtils.setViewBackground(feedHeader, gradient);
|
|
feedHeaderBorder.setBackgroundColor(Color.parseColor("#" + feedBorder));
|
|
|
|
if (TextUtils.equals(faviconText, "black")) {
|
|
binding.readingFeedTitle.setTextColor(UIUtils.getColor(getActivity(), R.color.text));
|
|
binding.readingFeedTitle.setShadowLayer(1, 0, 1, UIUtils.getColor(getActivity(), R.color.half_white));
|
|
} else {
|
|
binding.readingFeedTitle.setTextColor(UIUtils.getColor(getActivity(), R.color.white));
|
|
binding.readingFeedTitle.setShadowLayer(1, 0, 1, UIUtils.getColor(getActivity(), R.color.half_black));
|
|
}
|
|
|
|
if (!displayFeedDetails) {
|
|
binding.readingFeedTitle.setVisibility(View.GONE);
|
|
feedIcon.setVisibility(View.GONE);
|
|
} else {
|
|
FeedUtils.iconLoader.displayImage(feedIconUrl, feedIcon, 0, false);
|
|
binding.readingFeedTitle.setText(feedTitle);
|
|
}
|
|
|
|
itemDate.setText(StoryUtils.formatLongDate(getActivity(), story.timestamp));
|
|
|
|
if (story.tags.length <= 0) {
|
|
binding.readingItemTags.setVisibility(View.GONE);
|
|
}
|
|
|
|
binding.readingItemAuthors.setOnClickListener(new OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
if (story.feedId.equals("0")) return; // cannot train on feedless stories
|
|
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
|
|
intelFrag.show(getParentFragmentManager(), StoryIntelTrainerFragment.class.getName());
|
|
}
|
|
});
|
|
|
|
binding.readingFeedTitle.setOnClickListener(new OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
if (story.feedId.equals("0")) return; // cannot train on feedless stories
|
|
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
|
|
intelFrag.show(getParentFragmentManager(), StoryIntelTrainerFragment.class.getName());
|
|
}
|
|
});
|
|
|
|
binding.readingItemTitle.setOnClickListener(new OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
try {
|
|
UIUtils.handleUri(requireContext(), Uri.parse(story.permalink));
|
|
} catch (Throwable t) {
|
|
// we don't actually know if the user will successfully be able to open whatever string
|
|
// was in the permalink or if the Intent could throw errors
|
|
Log.e(this.getClass().getName(), "Error opening story by permalink URL.", t);
|
|
}
|
|
}
|
|
});
|
|
|
|
setupTagsAndIntel();
|
|
}
|
|
|
|
private void setupTagsAndIntel() {
|
|
int tag_green_text = UIUtils.getColor(getActivity(), R.color.tag_green_text);
|
|
int tag_red_text = UIUtils.getColor(getActivity(), R.color.tag_red_text);
|
|
Drawable tag_green_background = UIUtils.getDrawable(getActivity(), R.drawable.tag_background_positive);
|
|
Drawable tag_red_background = UIUtils.getDrawable(getActivity(), R.drawable.tag_background_negative);
|
|
|
|
binding.readingItemTags.removeAllViews();
|
|
for (String tag : story.tags) {
|
|
// TODO: these textviews with compound images are buggy, but stubbed in to let colourblind users
|
|
// see what is going on. these should be replaced with proper Chips when the v28 Chip lib
|
|
// is in full release.
|
|
View v = inflater.inflate(R.layout.tag_view, null);
|
|
|
|
TextView tagText = (TextView) v.findViewById(R.id.tag_text);
|
|
tagText.setText(tag);
|
|
|
|
if (classifier != null && classifier.tags.containsKey(tag)) {
|
|
switch (classifier.tags.get(tag)) {
|
|
case Classifier.LIKE:
|
|
UIUtils.setViewBackground(tagText, tag_green_background);
|
|
tagText.setTextColor(tag_green_text);
|
|
Drawable icon_like = UIUtils.getDrawable(getActivity(), R.drawable.ic_like_active);
|
|
icon_like.setBounds(0, 0, 30, 30);
|
|
tagText.setCompoundDrawables(null, null, icon_like, null);
|
|
tagText.setCompoundDrawablePadding(8);
|
|
break;
|
|
case Classifier.DISLIKE:
|
|
UIUtils.setViewBackground(tagText, tag_red_background);
|
|
tagText.setTextColor(tag_red_text);
|
|
Drawable icon_dislike = UIUtils.getDrawable(getActivity(), R.drawable.ic_dislike_active);
|
|
icon_dislike.setBounds(0, 0, 30, 30);
|
|
tagText.setCompoundDrawables(null, null, icon_dislike, null);
|
|
tagText.setCompoundDrawablePadding(8);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// tapping tags in saved stories doesn't bring up training
|
|
if (!(fs.isAllSaved() || (fs.getSingleSavedTag() != null))) {
|
|
v.setOnClickListener(new OnClickListener() {
|
|
@Override
|
|
public void onClick(View view) {
|
|
if (story.feedId.equals("0")) return; // cannot train on feedless stories
|
|
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
|
|
intelFrag.show(getParentFragmentManager(), StoryIntelTrainerFragment.class.getName());
|
|
}
|
|
});
|
|
}
|
|
|
|
binding.readingItemTags.addView(v);
|
|
}
|
|
|
|
if (!TextUtils.isEmpty(story.authors)) {
|
|
binding.readingItemAuthors.setText("• " + story.authors);
|
|
if (classifier != null && classifier.authors.containsKey(story.authors)) {
|
|
switch (classifier.authors.get(story.authors)) {
|
|
case Classifier.LIKE:
|
|
binding.readingItemAuthors.setTextColor(UIUtils.getColor(getActivity(), R.color.positive));
|
|
break;
|
|
case Classifier.DISLIKE:
|
|
binding.readingItemAuthors.setTextColor(UIUtils.getColor(getActivity(), R.color.negative));
|
|
break;
|
|
default:
|
|
binding.readingItemAuthors.setTextColor(UIUtils.getThemedColor(getActivity(), R.attr.readingItemMetadata, android.R.attr.textColor));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
String title = story.title;
|
|
title = UIUtils.colourTitleFromClassifier(title, classifier);
|
|
binding.readingItemTitle.setText(UIUtils.fromHtml(title));
|
|
}
|
|
|
|
public void switchSelectedViewMode() {
|
|
// if we were already in text mode, switch back to story mode
|
|
if (selectedFeedView == DefaultFeedView.TEXT) {
|
|
setViewMode(DefaultFeedView.STORY);
|
|
} else {
|
|
setViewMode(DefaultFeedView.TEXT);
|
|
}
|
|
|
|
Reading activity = (Reading) getActivity();
|
|
activity.viewModeChanged();
|
|
// telling the activity to change modes will chain a call to viewModeChanged()
|
|
}
|
|
|
|
private void setViewMode(DefaultFeedView newMode) {
|
|
selectedFeedView = newMode;
|
|
PrefsUtils.setDefaultViewModeForFeed(getActivity(), story.feedId, newMode);
|
|
}
|
|
|
|
public void viewModeChanged() {
|
|
synchronized (selectedFeedView) {
|
|
selectedFeedView = PrefsUtils.getDefaultViewModeForFeed(getActivity(), story.feedId);
|
|
}
|
|
// these can come from async tasks
|
|
Activity a = getActivity();
|
|
if (a != null) {
|
|
a.runOnUiThread(new Runnable() {
|
|
public void run() {
|
|
reloadStoryContent();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public DefaultFeedView getSelectedViewMode() {
|
|
return selectedFeedView;
|
|
}
|
|
|
|
private void reloadStoryContent() {
|
|
// reset indicators
|
|
binding.readingTextloading.setVisibility(View.GONE);
|
|
binding.readingTextmodefailed.setVisibility(View.GONE);
|
|
enableProgress(false);
|
|
|
|
boolean needStoryContent = false;
|
|
|
|
if (selectedFeedView == DefaultFeedView.STORY) {
|
|
needStoryContent = true;
|
|
} else {
|
|
if (textViewUnavailable) {
|
|
binding.readingTextmodefailed.setVisibility(View.VISIBLE);
|
|
needStoryContent = true;
|
|
} else if (originalText == null) {
|
|
binding.readingTextloading.setVisibility(View.VISIBLE);
|
|
enableProgress(true);
|
|
loadOriginalText();
|
|
// still show the story mode version, as the text mode one may take some time
|
|
needStoryContent = true;
|
|
} else {
|
|
setupWebview(originalText);
|
|
onContentLoadFinished();
|
|
}
|
|
}
|
|
|
|
if (needStoryContent) {
|
|
if (storyContent == null) {
|
|
loadStoryContent();
|
|
} else {
|
|
setupWebview(storyContent);
|
|
onContentLoadFinished();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void enableProgress(boolean loading) {
|
|
Activity parent = getActivity();
|
|
if (parent == null) return;
|
|
((Reading) parent).enableLeftProgressCircle(loading);
|
|
}
|
|
|
|
/**
|
|
* Lets the pager offer us an updated version of our story when a new cursor is
|
|
* cycled in. This class takes the responsibility of ensureing that the cursor
|
|
* index has not shifted, though, by checking story IDs.
|
|
*/
|
|
public void offerStoryUpdate(Story story) {
|
|
if (story == null) return;
|
|
if (! TextUtils.equals(story.storyHash, this.story.storyHash)) {
|
|
com.newsblur.util.Log.d(this, "prevented story list index offset shift");
|
|
return;
|
|
}
|
|
this.story = story;
|
|
//if (AppConstants.VERBOSE_LOG) com.newsblur.util.Log.d(this, "got fresh story");
|
|
}
|
|
|
|
public void handleUpdate(int updateType) {
|
|
if ((updateType & NbActivity.UPDATE_STORY) != 0) {
|
|
updateSaveButton();
|
|
updateShareButton();
|
|
setupItemCommentsAndShares();
|
|
}
|
|
if ((updateType & NbActivity.UPDATE_TEXT) != 0) {
|
|
reloadStoryContent();
|
|
}
|
|
if ((updateType & NbActivity.UPDATE_SOCIAL) != 0) {
|
|
updateShareButton();
|
|
setupItemCommentsAndShares();
|
|
}
|
|
if ((updateType & NbActivity.UPDATE_INTEL) != 0) {
|
|
classifier = FeedUtils.dbHelper.getClassifierForFeed(story.feedId);
|
|
setupTagsAndIntel();
|
|
}
|
|
}
|
|
|
|
private void loadOriginalText() {
|
|
if (story != null) {
|
|
new AsyncTask<Void, Void, String>() {
|
|
@Override
|
|
protected String doInBackground(Void... arg) {
|
|
return FeedUtils.getStoryText(story.storyHash);
|
|
}
|
|
@Override
|
|
protected void onPostExecute(String result) {
|
|
if (result != null) {
|
|
if (OriginalTextService.NULL_STORY_TEXT.equals(result)) {
|
|
// the server reported that text mode is not available. kick back to story mode
|
|
com.newsblur.util.Log.d(this, "orig text not avail for story: " + story.storyHash);
|
|
textViewUnavailable = true;
|
|
} else {
|
|
ReadingItemFragment.this.originalText = result;
|
|
}
|
|
reloadStoryContent();
|
|
} else {
|
|
com.newsblur.util.Log.d(this, "orig text not yet cached for story: " + story.storyHash);
|
|
OriginalTextService.addPriorityHash(story.storyHash);
|
|
triggerSync();
|
|
}
|
|
}
|
|
}.execute();
|
|
}
|
|
}
|
|
|
|
private void loadStoryContent() {
|
|
if (story == null) return;
|
|
new AsyncTask<Void, Void, String>() {
|
|
@Override
|
|
protected String doInBackground(Void... arg) {
|
|
return FeedUtils.getStoryContent(story.storyHash);
|
|
}
|
|
@Override
|
|
protected void onPostExecute(String result) {
|
|
if (result != null) {
|
|
ReadingItemFragment.this.storyContent = result;
|
|
reloadStoryContent();
|
|
} else {
|
|
com.newsblur.util.Log.w(this, "couldn't find story content for existing story.");
|
|
Activity act = getActivity();
|
|
if (act != null) act.finish();
|
|
}
|
|
}
|
|
}.execute();
|
|
}
|
|
|
|
private void setupWebview(final String storyText) {
|
|
if (getActivity() == null) {
|
|
// sometimes we get called before the activity is ready. abort, since we will get a refresh when
|
|
// the cursor loads
|
|
return;
|
|
}
|
|
getActivity().runOnUiThread(new Runnable() {
|
|
public void run() {
|
|
_setupWebview(storyText);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void _setupWebview(String storyText) {
|
|
if (getActivity() == null) {
|
|
// this method gets called by async UI bits that might hold stale fragment references with no assigned
|
|
// activity. If this happens, just abort the call.
|
|
return;
|
|
}
|
|
|
|
synchronized (WEBVIEW_CONTENT_MUTEX) {
|
|
// this method might get called repeatedly despite no content change, which is expensive
|
|
int contentHash = storyText.hashCode();
|
|
if (this.contentHash == contentHash) return;
|
|
this.contentHash = contentHash;
|
|
|
|
sniffAltTexts(storyText);
|
|
|
|
storyText = swapInOfflineImages(storyText);
|
|
|
|
float currentSize = PrefsUtils.getTextSize(getActivity());
|
|
Font font = PrefsUtils.getFont(getActivity());
|
|
ThemeValue themeValue = PrefsUtils.getSelectedTheme(getActivity());
|
|
|
|
StringBuilder builder = new StringBuilder();
|
|
builder.append("<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0\" />");
|
|
builder.append(font.forWebView(currentSize));
|
|
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"reading.css\" />");
|
|
if (themeValue == ThemeValue.LIGHT) {
|
|
// builder.append("<meta name=\"color-scheme\" content=\"light\"/>");
|
|
// builder.append("<meta name=\"supported-color-schemes\" content=\"light\"/>");
|
|
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"light_reading.css\" />");
|
|
} else if (themeValue == ThemeValue.DARK) {
|
|
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"dark_reading.css\" />");
|
|
} else if (themeValue == ThemeValue.BLACK) {
|
|
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"black_reading.css\" />");
|
|
} else if (themeValue == ThemeValue.AUTO) {
|
|
int nightModeFlags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
|
if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES) {
|
|
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"dark_reading.css\" />");
|
|
} else if (nightModeFlags == Configuration.UI_MODE_NIGHT_NO) {
|
|
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"light_reading.css\" />");
|
|
} else if (nightModeFlags == Configuration.UI_MODE_NIGHT_UNDEFINED) {
|
|
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"light_reading.css\" />");
|
|
}
|
|
}
|
|
builder.append("</head><body><div class=\"NB-story\">");
|
|
builder.append(storyText);
|
|
builder.append("</div></body></html>");
|
|
binding.readingWebview.loadDataWithBaseURL("file:///android_asset/", builder.toString(), "text/html", "UTF-8", null);
|
|
}
|
|
}
|
|
|
|
private static final Pattern altSniff1 = Pattern.compile("<img[^>]*src=(['\"])((?:(?!\\1).)*)\\1[^>]*alt=(['\"])((?:(?!\\3).)*)\\3[^>]*>", Pattern.CASE_INSENSITIVE);
|
|
private static final Pattern altSniff2 = Pattern.compile("<img[^>]*alt=(['\"])((?:(?!\\1).)*)\\1[^>]*src=(['\"])((?:(?!\\3).)*)\\3[^>]*>", Pattern.CASE_INSENSITIVE);
|
|
private static final Pattern altSniff3 = Pattern.compile("<img[^>]*src=(['\"])((?:(?!\\1).)*)\\1[^>]*title=(['\"])((?:(?!\\3).)*)\\3[^>]*>", Pattern.CASE_INSENSITIVE);
|
|
private static final Pattern altSniff4 = Pattern.compile("<img[^>]*title=(['\"])((?:(?!\\1).)*)\\1[^>]*src=(['\"])((?:(?!\\3).)*)\\3[^>]*>", Pattern.CASE_INSENSITIVE);
|
|
|
|
private void sniffAltTexts(String html) {
|
|
// Find images with alt tags and cache the text for use on long-press
|
|
// NOTE: if doing this via regex has a smell, you have a good nose! This method is far from perfect
|
|
// and may miss valid cases or trucate tags, but it works for popular feeds (read: XKCD) and doesn't
|
|
// require us to import a proper parser lib of hundreds of kilobytes just for this one feature.
|
|
imageAltTexts = new HashMap<String,String>();
|
|
// sniff for alts first
|
|
Matcher imgTagMatcher = altSniff1.matcher(html);
|
|
while (imgTagMatcher.find()) {
|
|
imageAltTexts.put(imgTagMatcher.group(2), imgTagMatcher.group(4));
|
|
}
|
|
imgTagMatcher = altSniff2.matcher(html);
|
|
while (imgTagMatcher.find()) {
|
|
imageAltTexts.put(imgTagMatcher.group(4), imgTagMatcher.group(2));
|
|
}
|
|
// then sniff for 'title' tags, so they will overwrite alts and take precedence
|
|
imgTagMatcher = altSniff3.matcher(html);
|
|
while (imgTagMatcher.find()) {
|
|
imageAltTexts.put(imgTagMatcher.group(2), imgTagMatcher.group(4));
|
|
}
|
|
imgTagMatcher = altSniff4.matcher(html);
|
|
while (imgTagMatcher.find()) {
|
|
imageAltTexts.put(imgTagMatcher.group(4), imgTagMatcher.group(2));
|
|
}
|
|
|
|
// while were are at it, create a place where we can later cache offline image remaps so that when
|
|
// we do an alt-text lookup, we can search for the right URL key.
|
|
imageUrlRemaps = new HashMap<String,String>();
|
|
}
|
|
|
|
private static final Pattern imgSniff = Pattern.compile("<img[^>]*(src\\s*=\\s*)\"([^\"]*)\"[^>]*>", Pattern.CASE_INSENSITIVE);
|
|
|
|
private String swapInOfflineImages(String html) {
|
|
Matcher imageTagMatcher = imgSniff.matcher(html);
|
|
while (imageTagMatcher.find()) {
|
|
String url = imageTagMatcher.group(2);
|
|
String localPath = FeedUtils.storyImageCache.getCachedLocation(url);
|
|
if (localPath == null) continue;
|
|
html = html.replace(imageTagMatcher.group(1)+"\""+url+"\"", "src=\""+localPath+"\"");
|
|
imageUrlRemaps.put(localPath, url);
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
/** We have pushed our desired content into the WebView. */
|
|
private void onContentLoadFinished() {
|
|
isContentLoadFinished = true;
|
|
checkLoadStatus();
|
|
}
|
|
|
|
/** The webview has finished loading our desired content. */
|
|
public void onWebLoadFinished() {
|
|
isWebLoadFinished = true;
|
|
checkLoadStatus();
|
|
}
|
|
|
|
/** The social UI has finished loading from the DB. */
|
|
public void onSocialLoadFinished() {
|
|
isSocialLoadFinished = true;
|
|
checkLoadStatus();
|
|
}
|
|
|
|
private void checkLoadStatus() {
|
|
synchronized (isLoadFinished) {
|
|
if (isContentLoadFinished && isWebLoadFinished && isSocialLoadFinished) {
|
|
// iff this is the first time all content has finished loading, trigger any UI
|
|
// behaviour that is position-dependent
|
|
if (!isLoadFinished) {
|
|
onLoadFinished();
|
|
}
|
|
isLoadFinished = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A hook for performing actions that need to happen after all of the view has loaded, including
|
|
* the story's HTML content, all metadata views, and all associated social views.
|
|
*/
|
|
private void onLoadFinished() {
|
|
// if there was a scroll position saved, restore it
|
|
if (savedScrollPosRel > 0f) {
|
|
// ScrollViews containing WebViews are very particular about call timing. since the inner view
|
|
// height can drastically change as viewport width changes, position has to be saved and restored
|
|
// as a proportion of total inner view height. that height won't be known until all the various
|
|
// async bits of the fragment have finished loading. however, even after the WebView calls back
|
|
// onProgressChanged with a value of 100, immediate calls to get the size of the view will return
|
|
// incorrect values. even posting a runnable to the very end of our UI event queue may be
|
|
// insufficient time to allow the WebView to actually finish internally computing state and size.
|
|
// an additional fixed delay is added in a last ditch attempt to give the black-box platform
|
|
// threads a chance to finish their work.
|
|
binding.readingScrollview.postDelayed(new Runnable() {
|
|
public void run() {
|
|
int relPos = Math.round(binding.readingScrollview.getChildAt(0).getMeasuredHeight() * savedScrollPosRel);
|
|
binding.readingScrollview.scrollTo(0, relPos);
|
|
}
|
|
}, 75L);
|
|
}
|
|
}
|
|
|
|
public void flagWebviewError() {
|
|
// TODO: enable a selective reload mechanism on load failures?
|
|
}
|
|
|
|
private class TextSizeReceiver extends BroadcastReceiver {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
binding.readingWebview.setTextSize(intent.getFloatExtra(TEXT_SIZE_VALUE, 1.0f));
|
|
}
|
|
}
|
|
|
|
private class ReadingFontReceiver extends BroadcastReceiver {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
contentHash = 0; // Force reload since content hasn't changed
|
|
reloadStoryContent();
|
|
}
|
|
}
|
|
} |