/*
* Copyright (c) 2015 Ha Duy Trung
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.hidroh.materialistic;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.widget.NestedScrollView;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.Toast;
import android.widget.ViewSwitcher;
import java.lang.ref.WeakReference;
import javax.inject.Inject;
import javax.inject.Named;
import io.github.hidroh.materialistic.annotation.Synthetic;
import io.github.hidroh.materialistic.data.Item;
import io.github.hidroh.materialistic.data.ItemManager;
import io.github.hidroh.materialistic.data.ReadabilityClient;
import io.github.hidroh.materialistic.data.ResponseListener;
import io.github.hidroh.materialistic.data.WebItem;
import io.github.hidroh.materialistic.widget.AdBlockWebViewClient;
import io.github.hidroh.materialistic.widget.CacheableWebView;
import io.github.hidroh.materialistic.widget.PopupMenu;
import io.github.hidroh.materialistic.widget.WebView;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
public class WebFragment extends LazyLoadFragment
implements Scrollable, KeyDelegate.BackInterceptor {
public static final String EXTRA_ITEM = WebFragment.class.getName() +".EXTRA_ITEM";
private static final String STATE_EMPTY = "state:empty";
private static final String STATE_READABILITY = "state:readability";
static final String ACTION_FULLSCREEN = WebFragment.class.getName() + ".ACTION_FULLSCREEN";
static final String EXTRA_FULLSCREEN = WebFragment.class.getName() + ".EXTRA_FULLSCREEN";
private static final String STATE_FULLSCREEN = "state:fullscreen";
private static final String STATE_CONTENT = "state:content";
private static final int DEFAULT_PROGRESS = 20;
@Synthetic WebView mWebView;
private NestedScrollView mScrollView;
@Synthetic boolean mExternalRequired = false;
@Inject @Named(ActivityModule.HN) ItemManager mItemManager;
@Inject PopupMenu mPopupMenu;
private KeyDelegate.NestedScrollViewHelper mScrollableHelper;
private final Preferences.Observable mPreferenceObservable = new Preferences.Observable();
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
setFullscreen(intent.getBooleanExtra(WebFragment.EXTRA_FULLSCREEN, false));
}
};
private ViewGroup mFullscreenView;
private ViewGroup mScrollViewContent;
@Synthetic ImageButton mButtonRefresh;
private ViewSwitcher mControls;
private EditText mEditText;
private View mButtonMore;
private View mButtonNext;
protected ProgressBar mProgressBar;
private boolean mFullscreen;
protected String mContent;
private AppUtils.SystemUiHelper mSystemUiHelper;
private View mFragmentView;
@Inject ReadabilityClient mReadabilityClient;
private WebItem mItem;
private boolean mIsHackerNewsUrl, mEmpty, mReadability;
@Override
public void onAttach(Context context) {
super.onAttach(context);
mPreferenceObservable.subscribe(context, this::onPreferenceChanged,
R.string.pref_readability_font,
R.string.pref_readability_line_height,
R.string.pref_readability_text_size);
LocalBroadcastManager.getInstance(context).registerReceiver(mReceiver,
new IntentFilter(ACTION_FULLSCREEN));
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mFullscreen = savedInstanceState.getBoolean(STATE_FULLSCREEN, false);
mContent = savedInstanceState.getString(STATE_CONTENT);
mEmpty = savedInstanceState.getBoolean(STATE_EMPTY, false);
mReadability = savedInstanceState.getBoolean(STATE_READABILITY, false);
mItem = savedInstanceState.getParcelable(EXTRA_ITEM);
} else {
mReadability = Preferences.getDefaultStoryView(getActivity()) ==
Preferences.StoryViewMode.Readability;
mItem = getArguments().getParcelable(EXTRA_ITEM);
}
mIsHackerNewsUrl = AppUtils.isHackerNewsUrl(mItem);
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (isNewInstance()) {
mFragmentView = inflater.inflate(R.layout.fragment_web, container, false);
mFullscreenView = (ViewGroup) mFragmentView.findViewById(R.id.fullscreen);
mScrollViewContent = (ViewGroup) mFragmentView.findViewById(R.id.scroll_view_content);
mScrollView = (NestedScrollView) mFragmentView.findViewById(R.id.nested_scroll_view);
mControls = (ViewSwitcher) mFragmentView.findViewById(R.id.control_switcher);
mWebView = (WebView) mFragmentView.findViewById(R.id.web_view);
mButtonRefresh = (ImageButton) mFragmentView.findViewById(R.id.button_refresh);
mButtonMore = mFragmentView.findViewById(R.id.button_more);
mButtonNext = mFragmentView.findViewById(R.id.button_next);
mButtonNext.setEnabled(false);
mEditText = (EditText) mFragmentView.findViewById(R.id.edittext);
setUpWebControls(mFragmentView);
setUpWebView(mFragmentView);
}
return mFragmentView;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setHasOptionsMenu(true);
if (isNewInstance()) {
mScrollableHelper = new KeyDelegate.NestedScrollViewHelper(mScrollView);
mSystemUiHelper = new AppUtils.SystemUiHelper(getActivity().getWindow());
mSystemUiHelper.setEnabled(!getResources().getBoolean(R.bool.multi_pane));
if (mFullscreen) {
setFullscreen(true);
}
}
}
@Override
protected void createOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_article, menu);
}
@Override
protected void prepareOptionsMenu(Menu menu) {
MenuItem menuReadability = menu.findItem(R.id.menu_readability);
menuReadability.setVisible(modeToggleEnabled());
mMenuTintDelegate.setIcon(menuReadability, mReadability ?
R.drawable.ic_web_black_24dp : R.drawable.ic_chrome_reader_mode_black_24dp);
menuReadability.setTitle(mReadability ? R.string.article : R.string.readability);
menu.findItem(R.id.menu_font_options).setVisible(fontEnabled());
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_font_options) {
showPreferences();
return true;
}
if (item.getItemId() == R.id.menu_readability) {
mReadability = !mReadability;
load();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onResume() {
super.onResume();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mWebView.onResume();
}
mWebView.resumeTimers();
}
@Override
public void onStop() {
super.onStop();
pauseWebView();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_FULLSCREEN, mFullscreen);
outState.putString(STATE_CONTENT, mContent);
outState.putParcelable(EXTRA_ITEM, mItem);
outState.putBoolean(STATE_EMPTY, mEmpty);
outState.putBoolean(STATE_READABILITY, mReadability);
}
@Override
public void onDestroy() {
super.onDestroy();
mWebView.destroy();
}
@Override
public void onDetach() {
super.onDetach();
mPreferenceObservable.unsubscribe(getActivity());
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mReceiver);
}
@Override
public void scrollToTop() {
if (mFullscreen) {
mWebView.pageUp(true);
} else {
mScrollableHelper.scrollToTop();
}
}
@Override
public boolean scrollToNext() {
if (mFullscreen) {
mWebView.pageDown(false);
return true;
} else {
return mScrollableHelper.scrollToNext();
}
}
@Override
public boolean scrollToPrevious() {
if (mFullscreen) {
mWebView.pageUp(false);
return true;
} else {
return mScrollableHelper.scrollToPrevious();
}
}
@Override
public boolean onBackPressed() {
if (mWebView.canGoBack()) {
mWebView.goBack();
return true;
}
return false;
}
@Override
protected void load() {
mWebView.setVisibility(View.INVISIBLE);
if (mIsHackerNewsUrl) {
bindContent();
} else if (mReadability && !mEmpty) {
if (TextUtils.isEmpty(mContent)) {
parse();
} else {
loadContent();
}
} else {
loadUrl();
}
}
private void loadUrl() {
setWebSettings(true);
mWebView.reloadUrl(mItem.getUrl());
}
@Synthetic
void loadContent() {
setWebSettings(false);
mWebView.reloadHtml(AppUtils.wrapHtml(getActivity(), mContent));
}
private void parse() {
mProgressBar.setProgress(DEFAULT_PROGRESS);
mReadabilityClient.parse(mItem.getId(), mItem.getUrl(), new ReadabilityCallback(this));
}
private void bindContent() {
if (mItem instanceof Item) {
mContent = ((Item) mItem).getText();
loadContent();
} else {
mItemManager.getItem(mItem.getId(), ItemManager.MODE_DEFAULT, new ItemResponseListener(this));
}
}
private void pauseWebView() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mWebView.onPause();
}
mWebView.pauseTimers();
}
private boolean fontEnabled() {
return mReadability && !mEmpty && !TextUtils.isEmpty(mContent);
}
private boolean modeToggleEnabled() {
return !mIsHackerNewsUrl && !mWebView.canGoBack();
}
private void setUpWebControls(View view) {
view.findViewById(R.id.toolbar_web).setOnClickListener(v -> scrollToTop());
view.findViewById(R.id.button_back).setOnClickListener(v -> mWebView.goBack());
view.findViewById(R.id.button_forward).setOnClickListener(v -> mWebView.goForward());
view.findViewById(R.id.button_clear).setOnClickListener(v -> {
mSystemUiHelper.setFullscreen(true);
reset();
mControls.showNext();
});
view.findViewById(R.id.button_find).setOnClickListener(v -> {
mEditText.requestFocus();
toggleSoftKeyboard(true);
mControls.showNext();
});
mButtonRefresh.setOnClickListener(v -> {
if (mWebView.getProgress() < 100) {
mWebView.stopLoading();
} else {
mWebView.reload();
}
});
view.findViewById(R.id.button_exit).setOnClickListener(v ->
LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(
new Intent(WebFragment.ACTION_FULLSCREEN)
.putExtra(EXTRA_FULLSCREEN, false)));
mButtonNext.setOnClickListener(v -> mWebView.findNext(true));
mButtonMore.setOnClickListener(v ->
mPopupMenu.create(getActivity(), mButtonMore, Gravity.NO_GRAVITY)
.inflate(R.menu.menu_web)
.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.menu_font_options) {
showPreferences();
return true;
}
if (item.getItemId() == R.id.menu_zoom_in) {
mWebView.zoomIn();
return true;
}
if (item.getItemId() == R.id.menu_zoom_out) {
mWebView.zoomOut();
return true;
}
return false;
})
.setMenuItemVisible(R.id.menu_font_options, fontEnabled())
.show());
mEditText.setOnEditorActionListener((v, actionId, event) -> { findInPage(); return true; });
}
private void setUpWebView(View view) {
mProgressBar = (ProgressBar) view.findViewById(R.id.progress);
mWebView.setBackgroundColor(Color.TRANSPARENT);
mWebView.setWebViewClient(new AdBlockWebViewClient(Preferences.adBlockEnabled(getActivity())) {
@Override
public void onPageStarted(android.webkit.WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
if (getActivity() != null) {
getActivity().supportInvalidateOptionsMenu();
}
}
@Override
public void onPageFinished(android.webkit.WebView view, String url) {
super.onPageFinished(view, url);
if (getActivity() != null) {
getActivity().supportInvalidateOptionsMenu();
}
}
});
mWebView.setWebChromeClient(new CacheableWebView.ArchiveClient() {
@Override
public void onProgressChanged(android.webkit.WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
mProgressBar.setProgress(newProgress);
mProgressBar.setVisibility(newProgress == 100 ? GONE : VISIBLE);
mButtonRefresh.setImageResource(newProgress == 100 ?
R.drawable.ic_refresh_white_24dp : R.drawable.ic_clear_white_24dp);
}
});
mWebView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> {
if (getActivity() == null) {
return;
}
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
if (intent.resolveActivity(getActivity().getPackageManager()) == null) {
return;
}
mExternalRequired = true;
mWebView.setVisibility(GONE);
view.findViewById(R.id.empty).setVisibility(VISIBLE);
view.findViewById(R.id.download_button).setOnClickListener(v -> startActivity(intent));
});
AppUtils.toggleWebViewZoom(mWebView.getSettings(), false);
}
@SuppressLint("SetJavaScriptEnabled")
private void setWebSettings(boolean isRemote) {
mReadability = !isRemote;
mWebView.setBackgroundColor(isRemote ? Color.WHITE : Color.TRANSPARENT);
mWebView.getSettings().setLoadWithOverviewMode(isRemote);
mWebView.getSettings().setUseWideViewPort(isRemote);
mWebView.getSettings().setJavaScriptEnabled(true);
getActivity().supportInvalidateOptionsMenu();
}
@Synthetic
void setFullscreen(boolean isFullscreen) {
if (getView() == null) {
return;
}
mFullscreen = isFullscreen;
mControls.setVisibility(isFullscreen ? VISIBLE : View.GONE);
AppUtils.toggleWebViewZoom(mWebView.getSettings(), isFullscreen);
ViewGroup.LayoutParams params = mWebView.getLayoutParams();
if (isFullscreen) {
mScrollView.removeView(mScrollViewContent);
mWebView.scrollTo(mScrollView.getScrollX(), mScrollView.getScrollY());
mFullscreenView.addView(mScrollViewContent);
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
} else {
reset();
mFullscreenView.removeView(mScrollViewContent);
mScrollView.addView(mScrollViewContent);
mScrollView.post(() -> mScrollView.scrollTo(mWebView.getScrollX(), mWebView.getScrollY()));
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
}
mWebView.setLayoutParams(params);
}
private void showPreferences() {
Bundle args = new Bundle();
args.putInt(PopupSettingsFragment.EXTRA_TITLE, R.string.font_options);
args.putIntArray(PopupSettingsFragment.EXTRA_XML_PREFERENCES,
new int[]{R.xml.preferences_readability});
((DialogFragment) Fragment.instantiate(getActivity(),
PopupSettingsFragment.class.getName(), args))
.show(getFragmentManager(), PopupSettingsFragment.class.getName());
}
private void onPreferenceChanged(int key, boolean contextChanged) {
if (!contextChanged) {
load();
}
}
private void reset() {
mEditText.setText(null);
mButtonNext.setEnabled(false);
toggleSoftKeyboard(false);
mWebView.clearMatches();
}
private void findInPage() {
String query = mEditText.getText().toString().trim();
if (TextUtils.isEmpty(query)) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mWebView.setFindListener((activeMatchOrdinal, numberOfMatches, isDoneCounting) -> {
if (isDoneCounting) {
handleFindResults(numberOfMatches);
}
});
mWebView.findAllAsync(query);
} else {
//noinspection deprecation
handleFindResults(mWebView.findAll(query));
}
}
private void handleFindResults(int numberOfMatches) {
mButtonNext.setEnabled(numberOfMatches > 0);
if (numberOfMatches == 0) {
Toast.makeText(getContext(), R.string.no_matches, Toast.LENGTH_SHORT).show();
} else {
toggleSoftKeyboard(false);
}
}
private void toggleSoftKeyboard(boolean visible) {
InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(
Context.INPUT_METHOD_SERVICE);
if (visible) {
imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT);
} else {
imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
}
}
@Synthetic
void onParsed(String content) {
if (isAttached()) {
mContent = content;
if (!TextUtils.isEmpty(mContent)) {
loadContent();
} else {
mEmpty = true;
if (mReadability) {
Toast.makeText(getActivity(), R.string.readability_failed, Toast.LENGTH_SHORT).show();
}
loadUrl();
}
}
}
@Synthetic
void onItemLoaded(@NonNull Item response) {
getActivity().supportInvalidateOptionsMenu();
mItem = response;
bindContent();
}
static class ReadabilityCallback implements ReadabilityClient.Callback {
private final WeakReference<WebFragment> mReadabilityFragment;
@Synthetic
ReadabilityCallback(WebFragment webFragment) {
mReadabilityFragment = new WeakReference<>(webFragment);
}
@Override
public void onResponse(String content) {
if (mReadabilityFragment.get() != null && mReadabilityFragment.get().isAttached()) {
mReadabilityFragment.get().onParsed(content);
}
}
}
static class ItemResponseListener implements ResponseListener<Item> {
private final WeakReference<WebFragment> mFragment;
@Synthetic
ItemResponseListener(WebFragment webFragment) {
mFragment = new WeakReference<>(webFragment);
}
@Override
public void onResponse(@Nullable Item response) {
if (mFragment.get() != null && mFragment.get().isAttached() && response != null) {
mFragment.get().onItemLoaded(response);
}
}
@Override
public void onError(String errorMessage) {
// do nothing
}
}
}