package org.wikipedia.page.bottomcontent; import android.graphics.Paint; import android.net.Uri; import android.support.annotation.NonNull; import android.support.design.widget.CoordinatorLayout; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.webkit.WebView; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ListAdapter; import android.widget.TextView; import com.facebook.drawee.view.SimpleDraweeView; import org.apache.commons.lang3.StringUtils; import org.json.JSONException; import org.json.JSONObject; import org.wikipedia.LongPressHandler.ListViewContextMenuListener; import org.wikipedia.R; import org.wikipedia.WikipediaApp; import org.wikipedia.analytics.SuggestedPagesFunnel; import org.wikipedia.bridge.CommunicationBridge; import org.wikipedia.history.HistoryEntry; import org.wikipedia.page.LinkHandler; import org.wikipedia.page.LinkMovementMethodExt; import org.wikipedia.page.Page; import org.wikipedia.page.PageContainerLongPressHandler; import org.wikipedia.page.PageFragment; import org.wikipedia.page.PageTitle; import org.wikipedia.page.SuggestionsTask; import org.wikipedia.search.SearchResult; import org.wikipedia.search.SearchResults; import org.wikipedia.util.DimenUtil; import org.wikipedia.util.StringUtil; import org.wikipedia.views.ConfigurableListView; import org.wikipedia.views.ConfigurableTextView; import org.wikipedia.views.GoneIfEmptyTextView; import org.wikipedia.views.ObservableWebView; import org.wikipedia.views.ViewUtil; import java.util.List; import static org.wikipedia.util.L10nUtil.formatDateRelative; import static org.wikipedia.util.L10nUtil.getStringForArticleLanguage; import static org.wikipedia.util.UriUtil.visitInExternalBrowser; public class BottomContentHandler implements BottomContentInterface, ObservableWebView.OnScrollChangeListener, ObservableWebView.OnContentHeightChangedListener { private static final String TAG = "BottomContentHandler"; private final PageFragment parentFragment; private final CommunicationBridge bridge; private final WebView webView; private final LinkHandler linkHandler; private PageTitle pageTitle; private final WikipediaApp app; private boolean firstTimeShown = false; private View bottomContentContainer; private TextView pageLastUpdatedText; private TextView pageLicenseText; private View readMoreContainer; private ConfigurableListView readMoreList; private SuggestedPagesFunnel funnel; private SearchResults readMoreItems; public BottomContentHandler(final PageFragment parentFragment, CommunicationBridge bridge, ObservableWebView webview, LinkHandler linkHandler, ViewGroup hidingView) { this.parentFragment = parentFragment; this.bridge = bridge; this.webView = webview; this.linkHandler = linkHandler; app = WikipediaApp.getInstance(); bottomContentContainer = hidingView; webview.addOnScrollChangeListener(this); webview.addOnContentHeightChangedListener(this); pageLastUpdatedText = (TextView) bottomContentContainer.findViewById(R.id.page_last_updated_text); pageLicenseText = (TextView) bottomContentContainer.findViewById(R.id.page_license_text); readMoreContainer = bottomContentContainer.findViewById(R.id.read_more_container); readMoreList = (ConfigurableListView) bottomContentContainer.findViewById(R.id.read_more_list); TextView pageExternalLink = (TextView) bottomContentContainer.findViewById(R.id.page_external_link); pageExternalLink.setPaintFlags(pageExternalLink.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); pageExternalLink.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { visitInExternalBrowser(parentFragment.getContext(), Uri.parse(pageTitle.getMobileUri())); } }); if (parentFragment.callback() != null) { ListViewContextMenuListener contextMenuListener = new LongPressHandler(parentFragment.callback()); new org.wikipedia.LongPressHandler(readMoreList, HistoryEntry.SOURCE_INTERNAL_LINK, contextMenuListener); } // set up pass-through scroll functionality for the ListView readMoreList.setOnTouchListener(new View.OnTouchListener() { private int touchSlop = ViewConfiguration.get(readMoreList.getContext()) .getScaledTouchSlop(); private boolean slopReached; private boolean doingSlopEvent; private boolean isPressed = false; private float startY; private float amountScrolled; @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getActionMasked() & MotionEvent.ACTION_MASK; switch (action) { case MotionEvent.ACTION_DOWN: isPressed = true; startY = event.getY(); amountScrolled = 0; slopReached = false; break; case MotionEvent.ACTION_MOVE: if (isPressed && !doingSlopEvent) { int contentHeight = (int)(webView.getContentHeight() * DimenUtil.getDensityScalar()); int maxScroll = contentHeight - webView.getScrollY() - webView.getHeight(); int scrollAmount = Math.min((int) (startY - event.getY()), maxScroll); // manually scroll the WebView that's underneath us... webView.scrollBy(0, scrollAmount); amountScrolled += scrollAmount; if (Math.abs(amountScrolled) > touchSlop && !slopReached) { slopReached = true; // send an artificial Move event that scrolls it by an amount // that's greater than the touch slop, so that the currently // highlighted item is unselected. MotionEvent moveEvent = MotionEvent.obtain(event); moveEvent.setLocation(event.getX(), event.getY() + touchSlop * 2); doingSlopEvent = true; readMoreList.dispatchTouchEvent(moveEvent); doingSlopEvent = false; } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: isPressed = false; break; default: break; } return false; } }); // hide ourselves by default hide(); } @Override public void onScrollChanged(int oldScrollY, int scrollY, boolean isHumanScroll) { if (bottomContentContainer.getVisibility() == View.GONE) { return; } int contentHeight = (int)(webView.getContentHeight() * DimenUtil.getDensityScalar()); int bottomOffset = contentHeight - scrollY - webView.getHeight(); int bottomHeight = bottomContentContainer.getHeight(); CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) bottomContentContainer.getLayoutParams(); if (bottomOffset > bottomHeight) { if (params.bottomMargin != -bottomHeight) { params.bottomMargin = -bottomHeight; params.topMargin = 0; bottomContentContainer.setLayoutParams(params); bottomContentContainer.setVisibility(View.INVISIBLE); } } else { params.bottomMargin = -bottomOffset; params.topMargin = -bottomHeight; bottomContentContainer.setLayoutParams(params); if (bottomContentContainer.getVisibility() != View.VISIBLE) { bottomContentContainer.setVisibility(View.VISIBLE); } if (!firstTimeShown && readMoreItems != null) { firstTimeShown = true; funnel.logSuggestionsShown(pageTitle, readMoreItems.getResults()); } } } @Override public void onContentHeightChanged(int contentHeight) { if (bottomContentContainer.getVisibility() != View.VISIBLE) { return; } // trigger a manual scroll event to update our position onScrollChanged(webView.getScrollY(), webView.getScrollY(), false); } /** * Hide the bottom content entirely. * It can only be shown again by calling beginLayout() */ @Override public void hide() { bottomContentContainer.setVisibility(View.GONE); } @Override public void beginLayout() { firstTimeShown = false; setupAttribution(); if (parentFragment.getPage().couldHaveReadMoreSection()) { preRequestReadMoreItems(parentFragment.getActivity().getLayoutInflater()); } else { bottomContentContainer.findViewById(R.id.read_more_container).setVisibility(View.GONE); layoutContent(); } } private void layoutContent() { if (!parentFragment.isAdded()) { return; } bottomContentContainer.setVisibility(View.INVISIBLE); // keep trying until our layout has a height... if (bottomContentContainer.getHeight() == 0) { final int postDelay = 50; bottomContentContainer.postDelayed(new Runnable() { @Override public void run() { layoutContent(); } }, postDelay); return; } // calculate the height of the listview, based on the number of items inside it. ListAdapter adapter = readMoreList.getAdapter(); if (adapter != null && adapter.getCount() > 0) { View item = View.inflate(readMoreList.getContext(), R.layout.item_page_list_entry, null); item.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); ViewGroup.LayoutParams params = readMoreList.getLayoutParams(); params.height = adapter.getCount() * item.getMeasuredHeight(); readMoreList.setLayoutParams(params); } readMoreList.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); bottomContentContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); // pad the bottom of the webview, to make room for ourselves int totalHeight = bottomContentContainer.getMeasuredHeight(); JSONObject payload = new JSONObject(); try { payload.put("paddingBottom", (int)(totalHeight / DimenUtil.getDensityScalar())); } catch (JSONException e) { throw new RuntimeException(e); } bridge.sendMessage("setPaddingBottom", payload); // ^ sending the padding event will guarantee a ContentHeightChanged event to be triggered, // which will update our margin based on the scroll offset, so we don't need to do it here. } private void setupAttribution() { Page page = parentFragment.getPage(); pageLicenseText.setText(StringUtil.fromHtml(String .format(parentFragment.getContext().getString(R.string.content_license_html), parentFragment.getContext().getString(R.string.cc_by_sa_3_url)))); pageLicenseText.setMovementMethod(new LinkMovementMethod()); // Don't display last updated message for main page or file pages, because it's always wrong if (page.isMainPage() || page.isFilePage()) { pageLastUpdatedText.setVisibility(View.GONE); } else { PageTitle title = page.getTitle(); String lastUpdatedHtml = "<a href=\"" + title.getUriForAction("history") + "\">" + parentFragment.getContext().getString(R.string.last_updated_text, formatDateRelative(page.getPageProperties().getLastModified()) + "</a>"); // TODO: Hide the Talk link if already on a talk page PageTitle talkPageTitle = new PageTitle("Talk", title.getPrefixedText(), title.getWikiSite()); String discussionHtml = "<a href=\"" + talkPageTitle.getCanonicalUri() + "\">" + parentFragment.getContext().getString(R.string.talk_page_link_text) + "</a>"; pageLastUpdatedText.setText(StringUtil.fromHtml(lastUpdatedHtml + " — " + discussionHtml)); pageLastUpdatedText.setMovementMethod(new LinkMovementMethodExt(linkHandler)); pageLastUpdatedText.setVisibility(View.VISIBLE); } } private void preRequestReadMoreItems(final LayoutInflater layoutInflater) { if (parentFragment.getPage().isMainPage()) { new MainPageReadMoreTopicTask(app) { @Override public void onFinish(HistoryEntry entry) { requestReadMoreItems(layoutInflater, entry); } @Override public void onCatch(Throwable caught) { // Read More titles are expendable. Log.w(TAG, "Error while getting Read More topic for main page.", caught); // but lay out the bottom content anyway: layoutContent(); } }.execute(); } else { requestReadMoreItems(layoutInflater, new HistoryEntry(pageTitle, HistoryEntry.SOURCE_INTERNAL_LINK)); } } private void requestReadMoreItems(final LayoutInflater layoutInflater, final HistoryEntry entry) { if (entry == null || TextUtils.isEmpty(entry.getTitle().getPrefixedText())) { hideReadMore(); layoutContent(); return; } final long timeMillis = System.currentTimeMillis(); new SuggestionsTask(app.getAPIForSite(entry.getTitle().getWikiSite()), entry.getTitle().getWikiSite(), entry.getTitle().getPrefixedText(), false) { @Override public void onFinish(SearchResults results) { funnel.setLatency(System.currentTimeMillis() - timeMillis); readMoreItems = results; if (!readMoreItems.getResults().isEmpty()) { // If there are results, set up section and make sure it's visible setUpReadMoreSection(layoutInflater, readMoreItems); showReadMore(); } else { // If there's no results, just hide the section hideReadMore(); } layoutContent(); } @Override public void onCatch(Throwable caught) { // Read More titles are expendable. Log.w(TAG, "Error while fetching Read More titles.", caught); // but lay out the bottom content anyway: layoutContent(); } }.execute(); } private void hideReadMore() { readMoreContainer.setVisibility(View.GONE); } private void showReadMore() { if (parentFragment.isAdded()) { ((ConfigurableTextView) readMoreContainer.findViewById(R.id.read_more_header)) .setText(getStringForArticleLanguage(parentFragment.getTitle(), R.string.read_more_section), pageTitle.getWikiSite().languageCode()); } readMoreContainer.setVisibility(View.VISIBLE); } @Override public PageTitle getTitle() { return pageTitle; } @Override public void setTitle(PageTitle newTitle) { pageTitle = newTitle; funnel = new SuggestedPagesFunnel(app); } private void setUpReadMoreSection(LayoutInflater layoutInflater, final SearchResults results) { final ReadMoreAdapter adapter = new ReadMoreAdapter(layoutInflater, results.getResults()); readMoreList.setAdapter(adapter, pageTitle.getWikiSite().languageCode()); readMoreList.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { PageTitle title = adapter.getItem(position).getPageTitle(); HistoryEntry historyEntry = new HistoryEntry(title, HistoryEntry.SOURCE_INTERNAL_LINK); parentFragment.loadPage(title, historyEntry); funnel.logSuggestionClicked(pageTitle, results.getResults(), position); } }); adapter.notifyDataSetChanged(); } private class LongPressHandler extends PageContainerLongPressHandler implements ListViewContextMenuListener { private int lastPosition; LongPressHandler(@NonNull PageFragment.Callback callback) { super(callback); } @Override public PageTitle getTitleForListPosition(int position) { lastPosition = position; return ((SearchResult) readMoreList.getAdapter().getItem(position)).getPageTitle(); } @Override public void onOpenLink(PageTitle title, HistoryEntry entry) { super.onOpenLink(title, entry); funnel.logSuggestionClicked(pageTitle, readMoreItems.getResults(), lastPosition); } @Override public void onOpenInNewTab(PageTitle title, HistoryEntry entry) { super.onOpenInNewTab(title, entry); funnel.logSuggestionClicked(pageTitle, readMoreItems.getResults(), lastPosition); } } private final class ReadMoreAdapter extends BaseAdapter { private final LayoutInflater inflater; private final List<SearchResult> results; private ReadMoreAdapter(LayoutInflater inflater, List<SearchResult> results) { this.inflater = inflater; this.results = results; } @Override public int getCount() { return results == null ? 0 : results.size(); } @Override public SearchResult getItem(int position) { return results.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(R.layout.item_page_list_entry, parent, false); } TextView pageTitleText = (TextView) convertView.findViewById(R.id.page_list_item_title); SearchResult result = getItem(position); pageTitleText.setText(result.getPageTitle().getDisplayText()); GoneIfEmptyTextView descriptionText = (GoneIfEmptyTextView) convertView.findViewById(R.id.page_list_item_description); descriptionText.setText(StringUtils.capitalize(result.getPageTitle().getDescription())); ViewUtil.loadImageUrlInto((SimpleDraweeView) convertView.findViewById(R.id.page_list_item_image), result.getPageTitle().getThumbUrl()); return convertView; } } }