org.wikipedia.page.bottomcontent.BottomContentHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.wikipedia.page.bottomcontent.BottomContentHandler.java

Source

package org.wikipedia.page.bottomcontent;

import android.text.Html;
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.FrameLayout;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;

import com.squareup.picasso.Picasso;

import org.json.JSONException;
import org.json.JSONObject;
import org.wikipedia.PageTitle;
import org.wikipedia.R;
import org.wikipedia.Utils;
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.PageActivity;
import org.wikipedia.page.PageViewFragmentInternal;
import org.wikipedia.page.SuggestionsTask;
import org.wikipedia.search.FullSearchArticlesTask;
import org.wikipedia.views.ObservableWebView;
import org.wikipedia.wikidata.WikidataCache;
import org.wikipedia.wikidata.WikidataDescriptionFeeder;

import java.util.List;
import java.util.Map;

public class BottomContentHandler
        implements ObservableWebView.OnScrollChangeListener, ObservableWebView.OnContentHeightChangedListener {
    private static final String TAG = "BottomContentHandler";
    private final PageViewFragmentInternal parentFragment;
    private final CommunicationBridge bridge;
    private final WebView webView;
    private final LinkHandler linkHandler;
    private PageTitle pageTitle;
    private final PageActivity activity;
    private final WikipediaApp app;
    private float displayDensity;
    private boolean firstTimeShown = false;

    private View bottomContentContainer;
    private TextView pageLastUpdatedText;
    private TextView pageLicenseText;
    private View readMoreContainer;
    private ListView readMoreList;

    private SuggestedPagesFunnel funnel;
    private FullSearchArticlesTask.FullSearchResults readMoreItems;

    public BottomContentHandler(PageViewFragmentInternal parentFragment, CommunicationBridge bridge,
            ObservableWebView webview, LinkHandler linkHandler, ViewGroup hidingView, PageTitle pageTitle) {
        this.parentFragment = parentFragment;
        this.bridge = bridge;
        this.webView = webview;
        this.linkHandler = linkHandler;
        this.pageTitle = pageTitle;
        activity = parentFragment.getActivity();
        app = (WikipediaApp) activity.getApplicationContext();
        displayDensity = activity.getResources().getDisplayMetrics().density;

        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 = (ListView) bottomContentContainer.findViewById(R.id.read_more_list);

        // 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() * displayDensity);
                        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;
            }
        });

        funnel = new SuggestedPagesFunnel(app, pageTitle.getSite());

        // preload the display density, since it will be used in a lot of places
        displayDensity = activity.getResources().getDisplayMetrics().density;
        // hide ourselves by default
        hide();
    }

    @Override
    public void onScrollChanged(int oldScrollY, int scrollY) {
        if (bottomContentContainer.getVisibility() == View.GONE) {
            return;
        }
        int contentHeight = (int) (webView.getContentHeight() * displayDensity);
        int bottomOffset = contentHeight - scrollY - webView.getHeight();
        int bottomHeight = bottomContentContainer.getHeight();
        FrameLayout.LayoutParams params = (FrameLayout.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.getPageTitles());
            }
        }
    }

    @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());
    }

    /**
     * Hide the bottom content entirely.
     * It can only be shown again by calling beginLayout()
     */
    public void hide() {
        bottomContentContainer.setVisibility(View.GONE);
    }

    public void beginLayout() {
        setupAttribution();
        if (parentFragment.getPage().couldHaveReadMoreSection()) {
            requestReadMoreItems(activity.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) {
            ViewGroup.LayoutParams params = readMoreList.getLayoutParams();
            final int itemHeight = (int) activity.getResources().getDimension(R.dimen.defaultListItemSize);
            params.height = adapter.getCount() * itemHeight
                    + (readMoreList.getDividerHeight() * (adapter.getCount() - 1));
            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 / displayDensity));
        } 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();
        String lastUpdatedHtml = "<a href=\"" + page.getTitle().getUriForAction("history") + "\">"
                + activity.getString(R.string.last_updated_text,
                        Utils.formatDateRelative(page.getPageProperties().getLastModified()) + "</a>");
        pageLastUpdatedText.setText(Html.fromHtml(lastUpdatedHtml));
        pageLastUpdatedText.setMovementMethod(new LinkMovementMethodExt(linkHandler));
        pageLicenseText.setText(Html.fromHtml(activity.getString(R.string.content_license_html)));
        pageLicenseText.setMovementMethod(new LinkMovementMethodExt(linkHandler));
    }

    private void requestReadMoreItems(final LayoutInflater layoutInflater) {
        new SuggestionsTask(app.getAPIForSite(pageTitle.getSite()), pageTitle.getSite(),
                pageTitle.getPrefixedText()) {
            @Override
            public void onFinish(FullSearchResults results) {
                readMoreItems = results;
                if (readMoreItems.getResults().size() > 0) {
                    // If there are results, set up section and make sure it's visible
                    setupReadMoreSection(layoutInflater, readMoreItems);
                    readMoreContainer.setVisibility(View.VISIBLE);
                } else {
                    // If there's no results, just hide the section
                    readMoreContainer.setVisibility(View.GONE);
                }
                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();
    }

    public PageTitle getTitle() {
        return pageTitle;
    }

    public void setTitle(PageTitle newTitle) {
        pageTitle = newTitle;
    }

    private void setupReadMoreSection(LayoutInflater layoutInflater,
            final FullSearchArticlesTask.FullSearchResults results) {
        final ReadMoreAdapter adapter = new ReadMoreAdapter(layoutInflater, results.getResults());
        readMoreList.setAdapter(adapter);
        readMoreList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                PageTitle title = (PageTitle) adapter.getItem(position);
                HistoryEntry historyEntry = new HistoryEntry(title, HistoryEntry.SOURCE_INTERNAL_LINK);
                // always add the description of the item to the cache so we don't even try to get it again
                app.getWikidataCache().put(title.toString(), title.getDescription());
                activity.displayNewPage(title, historyEntry);
                funnel.logSuggestionClicked(pageTitle, results.getPageTitles(), position);
            }
        });

        WikidataDescriptionFeeder.retrieveWikidataDescriptions(results.getResults(), app,
                new WikidataCache.OnWikidataReceiveListener() {
                    @Override
                    public void onWikidataReceived(Map<PageTitle, String> result) {
                        adapter.notifyDataSetChanged();
                    }

                    @Override
                    public void onWikidataFailed(Throwable caught) {
                        // Don't actually do anything.
                        // Descriptions are expendable
                    }
                });
    }

    private final class ReadMoreAdapter extends BaseAdapter {
        private final LayoutInflater inflater;
        private final List<PageTitle> results;

        private ReadMoreAdapter(LayoutInflater inflater, List<PageTitle> results) {
            this.inflater = inflater;
            this.results = results;
        }

        @Override
        public int getCount() {
            return results == null ? 0 : results.size();
        }

        @Override
        public Object 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_search_result, parent, false);
            }
            TextView pageTitleText = (TextView) convertView.findViewById(R.id.result_title);
            PageTitle result = (PageTitle) getItem(position);
            pageTitleText.setText(result.getDisplayText());

            TextView descriptionText = (TextView) convertView.findViewById(R.id.result_description);
            descriptionText.setText(result.getDescription());

            ImageView imageView = (ImageView) convertView.findViewById(R.id.result_image);
            String thumbnail = result.getThumbUrl();
            if (!app.showImages() || thumbnail == null) {
                Picasso.with(parent.getContext()).load(R.drawable.ic_pageimage_placeholder).into(imageView);
            } else {
                Picasso.with(parent.getContext()).load(thumbnail).placeholder(R.drawable.ic_pageimage_placeholder)
                        .error(R.drawable.ic_pageimage_placeholder).into(imageView);
            }

            return convertView;
        }
    }
}