Java tutorial
package org.wikipedia.page.bottomcontent; import android.app.Activity; import android.content.Context; import android.graphics.Paint; import android.net.Uri; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.webkit.WebView; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.json.JSONException; import org.json.JSONObject; import org.wikipedia.Constants; import org.wikipedia.R; import org.wikipedia.WikipediaApp; import org.wikipedia.analytics.SuggestedPagesFunnel; import org.wikipedia.bridge.CommunicationBridge; import org.wikipedia.dataclient.ServiceFactory; import org.wikipedia.dataclient.restbase.page.RbPageSummary; import org.wikipedia.history.HistoryEntry; import org.wikipedia.page.Namespace; import org.wikipedia.page.Page; import org.wikipedia.page.PageContainerLongPressHandler; import org.wikipedia.page.PageFragment; import org.wikipedia.page.PageTitle; import org.wikipedia.readinglist.ReadingListBookmarkMenu; import org.wikipedia.readinglist.database.ReadingList; import org.wikipedia.readinglist.database.ReadingListDbHelper; import org.wikipedia.readinglist.database.ReadingListPage; import org.wikipedia.util.DimenUtil; import org.wikipedia.util.FeedbackUtil; import org.wikipedia.util.GeoUtil; import org.wikipedia.util.L10nUtil; import org.wikipedia.util.StringUtil; import org.wikipedia.util.log.L; import org.wikipedia.views.ConfigurableTextView; import org.wikipedia.views.LinearLayoutOverWebView; import org.wikipedia.views.ObservableWebView; import org.wikipedia.views.PageItemView; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; import static org.wikipedia.Constants.InvokeSource.READ_MORE_BOOKMARK_BUTTON; import static org.wikipedia.util.L10nUtil.formatDateRelative; import static org.wikipedia.util.L10nUtil.getStringForArticleLanguage; import static org.wikipedia.util.L10nUtil.setConditionalLayoutDirection; import static org.wikipedia.util.UriUtil.visitInExternalBrowser; public class BottomContentView extends LinearLayoutOverWebView implements ObservableWebView.OnScrollChangeListener, ObservableWebView.OnContentHeightChangedListener { private PageFragment parentFragment; private WebView webView; private boolean firstTimeShown = false; private boolean webViewPadded = false; private int prevLayoutHeight; private Page page; @BindView(R.id.page_languages_container) View pageLanguagesContainer; @BindView(R.id.page_languages_divider) View pageLanguagesDivider; @BindView(R.id.page_languages_count_text) TextView pageLanguagesCount; @BindView(R.id.page_edit_history_container) View pageEditHistoryContainer; @BindView(R.id.page_edit_history_divider) View pageEditHistoryDivider; @BindView(R.id.page_last_updated_text) TextView pageLastUpdatedText; @BindView(R.id.page_talk_container) View pageTalkContainer; @BindView(R.id.page_talk_divider) View pageTalkDivider; @BindView(R.id.page_view_map_container) View pageMapContainer; @BindView(R.id.page_license_text) TextView pageLicenseText; @BindView(R.id.page_external_link) TextView pageExternalLink; @BindView(R.id.read_more_container) View readMoreContainer; @BindView(R.id.read_more_list) ListView readMoreList; private SuggestedPagesFunnel funnel; private ReadMoreAdapter readMoreAdapter = new ReadMoreAdapter(); private List<RbPageSummary> readMoreItems; private CommunicationBridge bridge; private CompositeDisposable disposables = new CompositeDisposable(); public BottomContentView(Context context) { super(context); init(); } public BottomContentView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public BottomContentView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { inflate(getContext(), R.layout.fragment_page_bottom_content, this); ButterKnife.bind(this); } public void setup(PageFragment parentFragment, CommunicationBridge bridge, ObservableWebView webview) { this.parentFragment = parentFragment; this.webView = webview; this.bridge = bridge; setWebView(webview); webview.addOnScrollChangeListener(this); webview.addOnContentHeightChangedListener(this); pageExternalLink.setPaintFlags(pageExternalLink.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); if (parentFragment.callback() != null) { org.wikipedia.LongPressHandler.ListViewOverflowMenuListener overflowMenuListener = new LongPressHandler( parentFragment); new org.wikipedia.LongPressHandler(readMoreList, HistoryEntry.SOURCE_INTERNAL_LINK, overflowMenuListener); } addOnLayoutChangeListener((View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) -> { if (prevLayoutHeight == getHeight()) { if (!webViewPadded) { padWebView(); } return; } prevLayoutHeight = getHeight(); padWebView(); }); readMoreList.setAdapter(readMoreAdapter); // hide ourselves by default hide(); } public void dispose() { disposables.clear(); } public void setPage(@NonNull Page page) { this.page = page; funnel = new SuggestedPagesFunnel(WikipediaApp.getInstance()); firstTimeShown = false; webViewPadded = false; setConditionalLayoutDirection(readMoreList, page.getTitle().getWikiSite().languageCode()); setupAttribution(); setupPageButtons(); if (page.couldHaveReadMoreSection()) { preRequestReadMoreItems(); } else { hideReadMore(); } setVisibility(View.INVISIBLE); } @OnClick(R.id.page_external_link) void onExternalLinkClick(View v) { visitInExternalBrowser(parentFragment.getContext(), Uri.parse(page.getTitle().getMobileUri())); } @OnClick(R.id.page_languages_container) void onLanguagesClick(View v) { parentFragment.startLangLinksActivity(); } @OnClick(R.id.page_edit_history_container) void onEditHistoryClick(View v) { visitInExternalBrowser(parentFragment.getContext(), Uri.parse(page.getTitle().getUriForAction("history"))); } @OnClick(R.id.page_talk_container) void onTalkClick(View v) { PageTitle title = page.getTitle(); PageTitle talkPageTitle = new PageTitle("Talk", title.getPrefixedText(), title.getWikiSite()); visitInExternalBrowser(parentFragment.getContext(), Uri.parse(talkPageTitle.getMobileUri())); } @OnClick(R.id.page_view_map_container) void onViewMapClick(View v) { GeoUtil.sendGeoIntent(parentFragment.requireActivity(), page.getPageProperties().getGeo(), page.getDisplayTitle()); } @Override public void onScrollChanged(int oldScrollY, int scrollY, boolean isHumanScroll) { if (getVisibility() == View.GONE) { return; } int contentHeight = (int) (webView.getContentHeight() * DimenUtil.getDensityScalar()); int bottomOffset = contentHeight - scrollY - webView.getHeight(); int bottomHeight = getHeight(); if (bottomOffset > bottomHeight) { setTranslationY(bottomHeight); if (getVisibility() != View.INVISIBLE) { setVisibility(View.INVISIBLE); } } else { setTranslationY(bottomOffset); if (getVisibility() != View.VISIBLE) { setVisibility(View.VISIBLE); } if (!firstTimeShown && readMoreItems != null) { firstTimeShown = true; readMoreAdapter.notifyDataSetChanged(); funnel.logSuggestionsShown(page.getTitle(), readMoreItems); } } } @Override public void onContentHeightChanged(int contentHeight) { perturb(); } /** * Hide the bottom content entirely. * It can only be shown again by calling beginLayout() */ public void hide() { setVisibility(View.GONE); } private void perturb() { webView.post(() -> { if (!parentFragment.isAdded()) { return; } // trigger a manual scroll event to update our position relative to the WebView. onScrollChanged(webView.getScrollY(), webView.getScrollY(), false); }); } private void padWebView() { // pad the bottom of the webview, to make room for ourselves JSONObject payload = new JSONObject(); try { payload.put("paddingBottom", (int) ((getHeight() + getResources().getDimension(R.dimen.bottom_toolbar_height)) / DimenUtil.getDensityScalar())); } catch (JSONException e) { throw new RuntimeException(e); } bridge.sendMessage("setPaddingBottom", payload); webViewPadded = true; // ^ 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. // And while we wait, let's make ourselves invisible, until we're made explicitly visible // by the scroll handler. setVisibility(View.INVISIBLE); } private void setupPageButtons() { // Don't display edit history for main page or file pages, because it's always wrong pageEditHistoryContainer.setVisibility((page.isMainPage() || page.isFilePage()) ? GONE : VISIBLE); pageLastUpdatedText.setText(parentFragment.getString(R.string.last_updated_text, formatDateRelative(page.getPageProperties().getLastModified()))); pageLastUpdatedText.setVisibility(View.VISIBLE); pageTalkContainer.setVisibility(page.getTitle().namespace() == Namespace.TALK ? GONE : VISIBLE); /** * TODO: It only updates the count when the article is in Chinese. * If an article is also available in Chinese, the count will be less one. * @see LangLinksActivity.java updateLanguageEntriesSupported() */ int getLanguageCount = L10nUtil.getUpdatedLanguageCountIfNeeded( page.getTitle().getWikiSite().languageCode(), page.getPageProperties().getLanguageCount()); pageLanguagesContainer.setVisibility(getLanguageCount == 0 ? GONE : VISIBLE); pageLanguagesCount.setText(parentFragment.getString(R.string.language_count_link_text, getLanguageCount)); pageMapContainer.setVisibility(page.getPageProperties().getGeo() == null ? GONE : VISIBLE); pageLanguagesDivider.setVisibility(pageLanguagesContainer.getVisibility()); pageTalkDivider.setVisibility(pageMapContainer.getVisibility()); } private void setupAttribution() { 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()); } private void preRequestReadMoreItems() { if (page.isMainPage()) { disposables.add(Observable.fromCallable(new MainPageReadMoreTopicTask()).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()).subscribe(this::requestReadMoreItems, throwable -> { L.w("Error while getting Read More topic for main page.", throwable); requestReadMoreItems(null); })); } else { requestReadMoreItems(new HistoryEntry(page.getTitle(), HistoryEntry.SOURCE_INTERNAL_LINK)); } } private void requestReadMoreItems(final HistoryEntry entry) { if (entry == null || TextUtils.isEmpty(entry.getTitle().getPrefixedText())) { hideReadMore(); return; } final long timeMillis = System.currentTimeMillis(); disposables.add(ServiceFactory.getRest(entry.getTitle().getWikiSite()) .getRelatedPages(entry.getTitle().getConvertedText()).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .map(response -> response.getPages(Constants.MAX_SUGGESTION_RESULTS * 2)).subscribe(results -> { funnel.setLatency(System.currentTimeMillis() - timeMillis); readMoreItems = results; if (readMoreItems != null && readMoreItems.size() > 0) { readMoreAdapter.setResults(results); showReadMore(); } else { // If there's no results, just hide the section hideReadMore(); } }, caught -> L.w("Error while fetching Read More titles.", caught))); } 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), page.getTitle().getWikiSite().languageCode()); } readMoreContainer.setVisibility(View.VISIBLE); } private class LongPressHandler extends PageContainerLongPressHandler implements org.wikipedia.LongPressHandler.ListViewOverflowMenuListener { private int lastPosition; LongPressHandler(@NonNull PageFragment fragment) { super(fragment); } @Override public PageTitle getTitleForListPosition(int position) { lastPosition = position; return ((RbPageSummary) readMoreList.getAdapter().getItem(position)) .getPageTitle(page.getTitle().getWikiSite()); } @Override public void onOpenLink(PageTitle title, HistoryEntry entry) { super.onOpenLink(title, entry); funnel.logSuggestionClicked(page.getTitle(), readMoreItems, lastPosition); } @Override public void onOpenInNewTab(PageTitle title, HistoryEntry entry) { super.onOpenInNewTab(title, entry); funnel.logSuggestionClicked(page.getTitle(), readMoreItems, lastPosition); } } private final class ReadMoreAdapter extends BaseAdapter implements PageItemView.Callback<RbPageSummary> { private List<RbPageSummary> results; public void setResults(List<RbPageSummary> results) { this.results = results; notifyDataSetChanged(); } @Override public int getCount() { return results == null ? 0 : Math.min(results.size(), Constants.MAX_SUGGESTION_RESULTS); } @Override public RbPageSummary getItem(int position) { return results.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convView, ViewGroup parent) { PageItemView<RbPageSummary> itemView = (PageItemView<RbPageSummary>) convView; if (itemView == null) { itemView = new PageItemView<>(getContext()); itemView.setLayoutParams(new ListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } RbPageSummary result = getItem(position); PageTitle pageTitle = result.getPageTitle(page.getTitle().getWikiSite()); itemView.setItem(result); ImageView primaryActionBtn = itemView.findViewById(R.id.page_list_item_action_primary); primaryActionBtn.setVisibility(VISIBLE); if (firstTimeShown) { setPrimaryActionDrawable(primaryActionBtn, pageTitle); } final int paddingEnd = 8; itemView.setPaddingRelative(itemView.getPaddingStart(), itemView.getPaddingTop(), DimenUtil.roundedDpToPx(paddingEnd), itemView.getPaddingBottom()); itemView.setCallback(this); itemView.setTitle(pageTitle.getDisplayText()); itemView.setDescription(StringUtils.capitalize(pageTitle.getDescription())); itemView.setImageUrl(pageTitle.getThumbUrl()); return itemView; } private void setPrimaryActionDrawable(ImageView primaryActionBtn, PageTitle pageTitle) { disposables.add(Observable .fromCallable(() -> ReadingListDbHelper.instance().findPageInAnyList(pageTitle) != null) .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) .subscribe(exists -> primaryActionBtn.setImageResource( exists ? R.drawable.ic_bookmark_white_24dp : R.drawable.ic_bookmark_border_black_24dp), L::w)); } @Override public void onClick(@Nullable RbPageSummary item) { PageTitle title = item.getPageTitle(page.getTitle().getWikiSite()); HistoryEntry historyEntry = new HistoryEntry(title, HistoryEntry.SOURCE_INTERNAL_LINK); parentFragment.loadPage(title, historyEntry); funnel.logSuggestionClicked(page.getTitle(), readMoreItems, results.indexOf(item)); } @Override public void onActionClick(@Nullable RbPageSummary item, @NonNull View view) { if (item == null) { return; } PageTitle pageTitle = item.getPageTitle(page.getTitle().getWikiSite()); disposables.add(Observable .fromCallable(() -> ReadingListDbHelper.instance().findPageInAnyList(pageTitle) != null) .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) .subscribe(pageInList -> { if (!pageInList) { parentFragment.addToReadingList(pageTitle, READ_MORE_BOOKMARK_BUTTON); } else { new ReadingListBookmarkMenu(view, new ReadingListBookmarkMenu.Callback() { @Override public void onAddRequest(@Nullable ReadingListPage page) { parentFragment.addToReadingList(pageTitle, READ_MORE_BOOKMARK_BUTTON); } @Override public void onDeleted(@Nullable ReadingListPage page) { FeedbackUtil.showMessage((Activity) getContext(), getContext().getString( R.string.reading_list_item_deleted, pageTitle.getDisplayText())); setPrimaryActionDrawable((ImageView) view, pageTitle); } @Override public void onShare() { // ignore } }).show(pageTitle); } }, L::w)); } @Override public boolean onLongClick(@Nullable RbPageSummary item) { return false; } @Override public void onThumbClick(@Nullable RbPageSummary item) { } @Override public void onSecondaryActionClick(@Nullable RbPageSummary item, @NonNull View view) { } @Override public void onListChipClick(@Nullable ReadingList readingList) { } } public void updateBookmark() { ((ReadMoreAdapter) readMoreList.getAdapter()).notifyDataSetChanged(); } }