package org.wikipedia.page; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.Nullable; import android.support.design.widget.TabLayout; import android.support.v4.content.ContextCompat; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import com.appenguin.onboarding.ToolTip; import org.json.JSONException; import org.json.JSONObject; import org.wikipedia.R; import org.wikipedia.WikipediaApp; import org.wikipedia.analytics.ToCInteractionFunnel; import org.wikipedia.bridge.CommunicationBridge; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.page.action.PageActionTab; import org.wikipedia.tooltip.ToolTipUtil; import org.wikipedia.util.DimenUtil; import org.wikipedia.util.StringUtil; import org.wikipedia.util.log.L; import org.wikipedia.views.ConfigurableListView; import org.wikipedia.views.WikiDrawerLayout; import java.lang.reflect.Field; import java.util.ArrayList; import static org.wikipedia.util.DimenUtil.getContentTopOffsetPx; import static org.wikipedia.util.L10nUtil.getStringForArticleLanguage; import static org.wikipedia.util.ResourceUtil.getThemedAttributeId; public class ToCHandler { private static final int MAX_LEVELS = 3; private static final int INDENTATION_WIDTH_DP = 16; private static final int READ_MORE_SECTION_ID = -1; private final ConfigurableListView tocList; private final ProgressBar tocProgress; private final CommunicationBridge bridge; private final WikiDrawerLayout slidingPane; private final TextView headerView; private final PageFragment fragment; private ToCInteractionFunnel funnel; @Nullable private DrawerLayout.DrawerListener drawerListener; /** * Flag to track if the drawer is closing because a link was clicked. * Used to make sure that we don't track closes that are caused by * the user clicking on a section. */ private boolean wasClicked = false; public ToCHandler(final PageFragment fragment, final WikiDrawerLayout slidingPane, final CommunicationBridge bridge) { this.fragment = fragment; this.bridge = bridge; this.slidingPane = slidingPane; this.tocList = (ConfigurableListView) slidingPane.findViewById(R.id.page_toc_list); ((FrameLayout.LayoutParams) tocList.getLayoutParams()).setMargins(0, getContentTopOffsetPx(fragment.getContext()), 0, 0); this.tocProgress = (ProgressBar) slidingPane.findViewById(R.id.page_toc_in_progress); bridge.addListener("currentSectionResponse", new CommunicationBridge.JSEventListener() { @Override public void onMessage(String messageType, JSONObject messagePayload) { int sectionID = messagePayload.optInt("sectionID"); L.d("current section is " + sectionID); if (tocList.getAdapter() == null) { return; } int itemToSelect = 0; // Find the list item that corresponds to the returned sectionID. // Start with index 1 of the list adapter, since index 0 is the header view, // and won't have a Section object associated with it. // And end with the second-to-last section, since the last section is the // artificial Read More section, and unknown to the WebView. // The lead section (id 0) will automatically fall through the loop. for (int i = 1; i < tocList.getAdapter().getCount() - 1; i++) { if (((Section) tocList.getAdapter().getItem(i)).getId() <= sectionID) { itemToSelect = i; } else { break; } } tocList.setItemChecked(itemToSelect, true); tocList.smoothScrollToPosition(itemToSelect); } }); headerView = (TextView) LayoutInflater.from(tocList.getContext()).inflate(R.layout.header_toc_list, null, false); tocList.addHeaderView(headerView); // create a dummy funnel, in case the drawer is pulled out before a page is loaded. funnel = new ToCInteractionFunnel(WikipediaApp.getInstance(), WikipediaApp.getInstance().getWikiSite(), 0, 0); drawerListener = new DrawerLayout.SimpleDrawerListener() { private boolean sectionRequested = false; @Override public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); fragment.getActivity().supportInvalidateOptionsMenu(); funnel.logOpen(); wasClicked = false; } @Override public void onDrawerClosed(View drawerView) { super.onDrawerClosed(drawerView); fragment.getActivity().supportInvalidateOptionsMenu(); if (!wasClicked) { funnel.logClose(); } sectionRequested = false; } @Override public void onDrawerSlide(View drawerView, float slideOffset) { super.onDrawerSlide(drawerView, slideOffset); // make sure the ActionBar is showing fragment.showToolbar(); fragment.getSearchBarHideHandler().setForceNoFade(slideOffset != 0); // request the current section to highlight, if we haven't yet if (!sectionRequested) { bridge.sendMessage("requestCurrentSection", new JSONObject()); sectionRequested = true; } } }; slidingPane.addDrawerListener(drawerListener); // todo: remove what was added } public void scrollToSection(String sectionAnchor) { JSONObject payload = new JSONObject(); try { payload.put("anchor", sectionAnchor); } catch (JSONException e) { throw new RuntimeException(e); } bridge.sendMessage("scrollToSection", payload); } public void scrollToSection(Section section) { if (section != null) { // is it the bottom (read more) section? if (section.getId() == READ_MORE_SECTION_ID) { bridge.sendMessage("scrollToBottom", new JSONObject()); } else { scrollToSection( section.isLead() ? "heading_" + section.getId() : section.getAnchor()); } } } public void setupToC(final Page page, WikiSite wiki, boolean firstPage) { tocProgress.setVisibility(View.GONE); tocList.setVisibility(View.VISIBLE); headerView.setText(StringUtil.fromHtml(page.getDisplayTitle())); headerView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { scrollToSection(page.getSections().get(0)); wasClicked = true; funnel.logClick(0, page.getTitle().getDisplayText()); hide(); } }); tocList.setAdapter(new ToCAdapter(page), wiki.languageCode()); tocList.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Section section = (Section) parent.getAdapter().getItem(position); scrollToSection(section); wasClicked = true; funnel.logClick(position, section.getHeading()); hide(); } }); funnel = new ToCInteractionFunnel(WikipediaApp.getInstance(), wiki, page.getPageProperties().getPageId(), tocList.getAdapter().getCount()); if (onboardingEnabled() && !page.isMainPage() && !firstPage) { showTocOnboarding(); } } public void show() { if (slidingPane.getSlidingEnabled(Gravity.END)) { slidingPane.openDrawer(GravityCompat.END); } } public void hide() { slidingPane.closeDrawer(GravityCompat.END); } public boolean isVisible() { return slidingPane.isDrawerOpen(GravityCompat.END); } public void setEnabled(boolean enabled) { slidingPane.setSlidingEnabled(enabled); } private boolean onboardingEnabled() { return WikipediaApp.getInstance().getOnboardingStateMachine().isTocTutorialEnabled(); } private final class ToCAdapter extends BaseAdapter { private final ArrayList<Section> sections; private ToCAdapter(Page page) { sections = new ArrayList<>(); for (Section s : page.getSections()) { if (s.getLevel() < MAX_LEVELS && !s.isLead()) { sections.add(s); } } if (page.couldHaveReadMoreSection()) { // add a fake section at the end to represent the "read more" contents at the bottom: sections.add(new Section(READ_MORE_SECTION_ID, 0, getStringForArticleLanguage(page.getTitle(), R.string.read_more_section), "", "")); } } @Override public int getCount() { return sections.size(); } @Override public Section getItem(int position) { return sections.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_toc_entry, parent, false); } Section section = getItem(position); TextView sectionHeading = (TextView) convertView.findViewById(R.id.page_toc_item_text); View sectionFiller = convertView.findViewById(R.id.page_toc_filler); LinearLayout.LayoutParams indentLayoutParameters = new LinearLayout.LayoutParams(sectionFiller.getLayoutParams()); indentLayoutParameters.width = (section.getLevel() - 1) * (int) (INDENTATION_WIDTH_DP * DimenUtil.getDensityScalar()); sectionFiller.setLayoutParams(indentLayoutParameters); sectionHeading.setText(StringUtil.fromHtml(section.getHeading())); if (section.getLevel() > 1) { sectionHeading.setTextColor( getColor(getThemedAttributeId(fragment.getContext(), R.attr.toc_subsection_text_color))); } else { sectionHeading.setTextColor( getColor(getThemedAttributeId(fragment.getContext(), R.attr.toc_section_text_color))); } return convertView; } } private void showTocOnboarding() { TabLayout pageActionTabLayout = (TabLayout) fragment.getActivity().findViewById(R.id.page_actions_tab_layout); TabLayout.Tab tocTab = pageActionTabLayout.getTabAt(PageActionTab.VIEW_TOC.code()); try { Field f = tocTab.getClass().getDeclaredField("mView"); f.setAccessible(true); View tabView = (View) f.get(tocTab); ToolTipUtil.showToolTip(fragment.getActivity(), tabView, R.layout.inflate_tool_tip_toc_button, ToolTip.Position.CENTER); WikipediaApp.getInstance().getOnboardingStateMachine().setTocTutorial(); } catch (Exception e) { // If this fails once it will likely always fail for the same reason, so let's prevent // the onboarding from being attempted and failing on every page view forever. WikipediaApp.getInstance().getOnboardingStateMachine().setTocTutorial(); L.w("ToC onboarding failed", e); } } @ColorInt private int getColor(@ColorRes int id) { return ContextCompat.getColor(WikipediaApp.getInstance(), id); } }