jenkins-bot has submitted this change and it was merged. Change subject: Reuse the same WebView for article navigation. ......................................................................
Reuse the same WebView for article navigation. The plan so far: - PageViewFragmentInternal shall now become a true Fragment (and I've deleted the "old" PageViewFragment class). Once this patch is merged, we can rename it back to PageViewFragment. I'm not renaming it yet, to better keep track of what is changing. - The new fragment structure of the main activity shall be: a single PageViewFragment (to display n pages), and at most one other fragment on top of it (History, Saved Pages, Nearby) for selecting pages to navigate to. - Added additional sequencing logic to async tasks that fetch page content, so that they correctly invalidate themselves when a new page is loaded. TODO: Perform some additional profiling, and see if there are any remaining resource leaks. Change-Id: I40ac4c6f1a0351e7fec7b462d5b950c12fd09122 --- M wikipedia/assets/bundle.js M wikipedia/src/main/java/org/wikipedia/NavDrawerFragment.java M wikipedia/src/main/java/org/wikipedia/analytics/SuggestedPagesFunnel.java M wikipedia/src/main/java/org/wikipedia/editing/EditHandler.java M wikipedia/src/main/java/org/wikipedia/page/PageActivity.java A wikipedia/src/main/java/org/wikipedia/page/PageBackStackItem.java M wikipedia/src/main/java/org/wikipedia/page/PageCache.java M wikipedia/src/main/java/org/wikipedia/page/PageInfoHandler.java D wikipedia/src/main/java/org/wikipedia/page/PageViewFragment.java M wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java M wikipedia/src/main/java/org/wikipedia/page/ToCHandler.java M wikipedia/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandler.java M wikipedia/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandlerOld.java M wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryActivity.java M wikipedia/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java M www/js/sections.js 16 files changed, 539 insertions(+), 456 deletions(-) Approvals: BearND: Looks good to me, approved jenkins-bot: Verified diff --git a/wikipedia/assets/bundle.js b/wikipedia/assets/bundle.js index 3f1add0..a17d6a6 100644 --- a/wikipedia/assets/bundle.js +++ b/wikipedia/assets/bundle.js @@ -398,6 +398,17 @@ document.getElementById( "content" ).style.paddingBottom = payload.paddingBottom + "px"; }); +bridge.registerListener( "beginNewPage", function( payload ) { + clearContents(); + // fire an event back to the app, but with a slight timeout, which should + // have the effect of "waiting" until the page contents have cleared before sending the + // event, allowing synchronization of sorts between the WebView and the app. + // (If we find a better way to synchronize the two, it can be done here, as well) + setTimeout( function() { + bridge.sendMessage( "onBeginNewPage", payload ); + }, 10); +}); + bridge.registerListener( "displayLeadSection", function( payload ) { // This might be a refresh! Clear out all contents! clearContents(); @@ -518,7 +529,7 @@ scrolledOnLoad = true; } document.getElementById( "loading_sections").className = ""; - bridge.sendMessage( "pageLoadComplete", { } ); + bridge.sendMessage( "pageLoadComplete", { "sequence": payload.sequence } ); } else { var contentWrapper = document.getElementById( "content" ); elementsForSection(payload.section).forEach(function (element) { @@ -533,7 +544,7 @@ if ( typeof payload.fragment === "string" && payload.fragment.length > 0 && payload.section.anchor === payload.fragment) { scrollToSection( payload.fragment ); } - bridge.sendMessage( "requestSection", { "index": payload.section.id + 1 }); + bridge.sendMessage( "requestSection", { "sequence": payload.sequence, "index": payload.section.id + 1 }); } }); diff --git a/wikipedia/src/main/java/org/wikipedia/NavDrawerFragment.java b/wikipedia/src/main/java/org/wikipedia/NavDrawerFragment.java index fd3a948..fba6da3 100644 --- a/wikipedia/src/main/java/org/wikipedia/NavDrawerFragment.java +++ b/wikipedia/src/main/java/org/wikipedia/NavDrawerFragment.java @@ -20,7 +20,7 @@ import org.wikipedia.login.LoginActivity; import org.wikipedia.nearby.NearbyFragment; import org.wikipedia.page.PageActivity; -import org.wikipedia.page.PageViewFragment; +import org.wikipedia.page.PageViewFragmentInternal; import org.wikipedia.random.RandomHandler; import org.wikipedia.savedpages.SavedPagesFragment; import org.wikipedia.settings.SettingsActivity; @@ -138,12 +138,14 @@ highlightItem = R.id.nav_item_saved_pages; } else if (activity.getTopFragment() instanceof NearbyFragment) { highlightItem = R.id.nav_item_nearby; - } else if (activity.getTopFragment() instanceof PageViewFragment) { - PageViewFragment fragment = (PageViewFragment)activity.getTopFragment(); - if (fragment.getFragment().getHistoryEntry().getSource() == HistoryEntry.SOURCE_MAIN_PAGE) { - highlightItem = R.id.nav_item_today; - } else if (fragment.getFragment().getHistoryEntry().getSource() == HistoryEntry.SOURCE_RANDOM) { - highlightItem = R.id.nav_item_random; + } else if (activity.getTopFragment() instanceof PageViewFragmentInternal) { + PageViewFragmentInternal fragment = (PageViewFragmentInternal)activity.getTopFragment(); + if (fragment.getHistoryEntry() != null) { + if (fragment.getHistoryEntry().getSource() == HistoryEntry.SOURCE_MAIN_PAGE) { + highlightItem = R.id.nav_item_today; + } else if (fragment.getHistoryEntry().getSource() == HistoryEntry.SOURCE_RANDOM) { + highlightItem = R.id.nav_item_random; + } } } if (highlightItem != -1) { diff --git a/wikipedia/src/main/java/org/wikipedia/analytics/SuggestedPagesFunnel.java b/wikipedia/src/main/java/org/wikipedia/analytics/SuggestedPagesFunnel.java index 91e1f80..b39f51c 100644 --- a/wikipedia/src/main/java/org/wikipedia/analytics/SuggestedPagesFunnel.java +++ b/wikipedia/src/main/java/org/wikipedia/analytics/SuggestedPagesFunnel.java @@ -39,12 +39,13 @@ return eventData; } - protected void log(Object... params) { + protected void log(Site site, Object... params) { super.log(site, params); } public void logSuggestionsShown(PageTitle currentPageTitle, List<PageTitle> suggestedTitles) { log( + currentPageTitle.getSite(), "action", "shown", "pageTitle", currentPageTitle.getDisplayText(), "readMoreList", TextUtils.join("|", suggestedTitles) @@ -54,6 +55,7 @@ public void logSuggestionClicked(PageTitle currentPageTitle, List<PageTitle> suggestedTitles, int clickedIndex) { log( + currentPageTitle.getSite(), "action", "clicked", "pageTitle", currentPageTitle.getDisplayText(), "readMoreList", TextUtils.join("|", suggestedTitles), diff --git a/wikipedia/src/main/java/org/wikipedia/editing/EditHandler.java b/wikipedia/src/main/java/org/wikipedia/editing/EditHandler.java index f270cda..bd79530 100644 --- a/wikipedia/src/main/java/org/wikipedia/editing/EditHandler.java +++ b/wikipedia/src/main/java/org/wikipedia/editing/EditHandler.java @@ -12,17 +12,17 @@ import org.wikipedia.history.HistoryEntry; import org.wikipedia.page.Page; import org.wikipedia.page.PageActivity; -import org.wikipedia.page.PageViewFragment; +import org.wikipedia.page.PageViewFragmentInternal; import org.wikipedia.page.Section; public class EditHandler implements CommunicationBridge.JSEventListener { public static final int RESULT_REFRESH_PAGE = 1; - private final PageViewFragment fragment; + private final PageViewFragmentInternal fragment; private ProtectedEditAttemptFunnel funnel; private Page currentPage; - public EditHandler(PageViewFragment fragment, CommunicationBridge bridge) { + public EditHandler(PageViewFragmentInternal fragment, CommunicationBridge bridge) { this.fragment = fragment; bridge.addListener("editSectionClicked", this); } @@ -59,7 +59,7 @@ } if (messageType.equals("editSectionClicked")) { final SavedPagesFunnel savedPagesFunnel = WikipediaApp.getInstance().getFunnelManager().getSavedPagesFunnel(currentPage.getTitle().getSite()); - if (fragment.getFragment().getHistoryEntry().getSource() == HistoryEntry.SOURCE_SAVED_PAGE) { + if (fragment.getHistoryEntry().getSource() == HistoryEntry.SOURCE_SAVED_PAGE) { savedPagesFunnel.logEditAttempt(); new AlertDialog.Builder(fragment.getActivity()) .setCancelable(false) @@ -67,7 +67,7 @@ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { - fragment.getFragment().refreshPage(true); + fragment.refreshPage(true); savedPagesFunnel.logEditRefresh(); wasRefreshed = true; } diff --git a/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java b/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java index 152029b..df4ad6b 100644 --- a/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java +++ b/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java @@ -121,8 +121,8 @@ */ public PageViewFragmentInternal getCurPageFragment() { Fragment f = getTopFragment(); - if (f instanceof PageViewFragment) { - return ((PageViewFragment)f).getFragment(); + if (f instanceof PageViewFragmentInternal) { + return (PageViewFragmentInternal) f; } else { return null; } @@ -445,6 +445,20 @@ } /** + * Reset our fragment structure to the default, which is simply one PageViewFragment + * on the fragment stack. + * @param allowStateLoss Whether to allow state loss. + */ + private void resetFragments(boolean allowStateLoss) { + while (getTopFragment() != null && !(getTopFragment() instanceof PageViewFragmentInternal)) { + getSupportFragmentManager().popBackStackImmediate(); + } + if (getTopFragment() == null) { + pushFragment(new PageViewFragmentInternal(), allowStateLoss); + } + } + + /** * Add a new fragment to the top of the activity's backstack. * @param f New fragment to place on top. */ @@ -463,36 +477,20 @@ searchBarHideHandler.setForceNoFade(false); searchBarHideHandler.setFadeEnabled(false); // if the new fragment is the same class as the current topmost fragment, - // then just keep the previous fragment there. (unless it's a PageViewFragment) + // then just keep the previous fragment there. // e.g. if the user selected History, and there's already a History fragment on top, // then there's no need to load a new History fragment. - if (getTopFragment() != null - && (getTopFragment().getClass() == f.getClass()) - && !(f instanceof PageViewFragment)) { + if (getTopFragment() != null && (getTopFragment().getClass() == f.getClass())) { return; } - - // if the fragment onto which the new fragment is being placed is not a PageViewFragment, - // then remove it from the backstack (to comply with Google navigation guidelines). - if (getTopFragment() != null && !(getTopFragment() instanceof PageViewFragment)) { - getSupportFragmentManager().popBackStackImmediate(); - } - int totalFragments = getSupportFragmentManager().getBackStackEntryCount(); + if (totalFragments > 0) { + resetFragments(allowStateLoss); + } FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); // do an animation on the new fragment, but only if there was a previous one before it. - if (getTopFragment() != null) { - if (f instanceof PageViewFragment) { - // animate page fragments by sliding them in/out - trans.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, - R.anim.slide_in_left, R.anim.slide_out_right); - } else { - // animate other fragments (history/saved/etc) by fading them in/out - trans.setCustomAnimations(R.anim.abc_fade_in, R.anim.abc_fade_out, 0, 0); - } - } - + trans.setCustomAnimations(R.anim.abc_fade_in, R.anim.abc_fade_out, 0, 0); trans.replace(R.id.content_fragment_container, f, "fragment_" + Integer.toString(totalFragments)); trans.addToBackStack(null); if (allowStateLoss) { @@ -537,7 +535,7 @@ * @param entry HistoryEntry associated with this page. * @param allowStateLoss Whether to allow state loss. */ - public void displayNewPage(PageTitle title, HistoryEntry entry, boolean allowStateLoss) { + public void displayNewPage(final PageTitle title, final HistoryEntry entry, boolean allowStateLoss) { ACRA.getErrorReporter().putCustomData("api", title.getSite().getApiDomain()); ACRA.getErrorReporter().putCustomData("title", title.toString()); @@ -548,23 +546,29 @@ Utils.visitInExternalBrowser(this, Uri.parse(title.getMobileUri())); return; } + resetFragments(allowStateLoss); - //is the new title the same as what's already being displayed? - if (getTopFragment() instanceof PageViewFragment) { - PageViewFragmentInternal frag = getCurPageFragment(); - if (frag.getTitle().equals(title)) { - //if we have a section to scroll to, then pass it to the fragment - if (!TextUtils.isEmpty(title.getFragment())) { - frag.scrollToSection(title.getFragment()); + fragmentContainerView.post(new Runnable() { + @Override + public void run() { + //is the new title the same as what's already being displayed? + PageViewFragmentInternal frag = getCurPageFragment(); + if (frag.getTitle() != null && frag.getTitle().equals(title)) { + //if we have a section to scroll to, then pass it to the fragment + if (!TextUtils.isEmpty(title.getFragment())) { + frag.scrollToSection(title.getFragment()); + } + return; } - return; + frag.closeFindInPage(); + // Load the new page from cache (if possible) by default, unless it's the + // Main Page, in which case we'll always bypass the cache, because the Main + // Page is updated daily. + boolean loadFromCache = entry.getSource() != HistoryEntry.SOURCE_MAIN_PAGE; + frag.displayNewPage(title, entry, loadFromCache, true); + app.getSessionFunnel().pageViewed(entry); } - frag.closeFindInPage(); - } - - pushFragment(PageViewFragment.newInstance(title, entry), allowStateLoss); - - app.getSessionFunnel().pageViewed(entry); + }); } /** diff --git a/wikipedia/src/main/java/org/wikipedia/page/PageBackStackItem.java b/wikipedia/src/main/java/org/wikipedia/page/PageBackStackItem.java new file mode 100644 index 0000000..eebdbd2 --- /dev/null +++ b/wikipedia/src/main/java/org/wikipedia/page/PageBackStackItem.java @@ -0,0 +1,52 @@ +package org.wikipedia.page; + +import org.wikipedia.PageTitle; +import org.wikipedia.history.HistoryEntry; +import android.os.Parcel; +import android.os.Parcelable; + +public class PageBackStackItem implements Parcelable { + private final PageTitle title; + public PageTitle getTitle() { return title; } + + private final HistoryEntry historyEntry; + public HistoryEntry getHistoryEntry() { return historyEntry; } + + private int scrollY; + public int getScrollY() { return scrollY; } + public void setScrollY(int scrollY) { this.scrollY = scrollY; } + + public PageBackStackItem(PageTitle title, HistoryEntry historyEntry) { + this.title = title; + this.historyEntry = historyEntry; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(title, flags); + dest.writeParcelable(historyEntry, flags); + dest.writeInt(scrollY); + } + + private PageBackStackItem(Parcel in) { + title = in.readParcelable(PageTitle.class.getClassLoader()); + historyEntry = in.readParcelable(HistoryEntry.class.getClassLoader()); + scrollY = in.readInt(); + } + + public static final Parcelable.Creator<PageBackStackItem> CREATOR + = new Parcelable.Creator<PageBackStackItem>() { + public PageBackStackItem createFromParcel(Parcel in) { + return new PageBackStackItem(in); + } + + public PageBackStackItem[] newArray(int size) { + return new PageBackStackItem[size]; + } + }; +} diff --git a/wikipedia/src/main/java/org/wikipedia/page/PageCache.java b/wikipedia/src/main/java/org/wikipedia/page/PageCache.java index d67ed65..579cccd 100644 --- a/wikipedia/src/main/java/org/wikipedia/page/PageCache.java +++ b/wikipedia/src/main/java/org/wikipedia/page/PageCache.java @@ -116,20 +116,20 @@ } public interface CacheGetListener { - void onGetComplete(Page page); - void onGetError(Throwable e); + void onGetComplete(Page page, int sequence); + void onGetError(Throwable e, int sequence); } - public void get(PageTitle title, final CacheGetListener listener) { + public void get(PageTitle title, final int sequence, final CacheGetListener listener) { new GetPageFromCacheTask(title) { @Override public void onFinish(Page page) { - listener.onGetComplete(page); + listener.onGetComplete(page, sequence); } @Override public void onCatch(Throwable caught) { - listener.onGetError(caught); + listener.onGetError(caught, sequence); } }.execute(); } diff --git a/wikipedia/src/main/java/org/wikipedia/page/PageInfoHandler.java b/wikipedia/src/main/java/org/wikipedia/page/PageInfoHandler.java index 262ca5b..2771df7 100644 --- a/wikipedia/src/main/java/org/wikipedia/page/PageInfoHandler.java +++ b/wikipedia/src/main/java/org/wikipedia/page/PageInfoHandler.java @@ -14,11 +14,9 @@ */ abstract class PageInfoHandler implements CommunicationBridge.JSEventListener { private final PageActivity activity; - private final Site site; - PageInfoHandler(PageActivity activity, CommunicationBridge bridge, Site site) { + PageInfoHandler(PageActivity activity, CommunicationBridge bridge) { this.activity = activity; - this.site = site; bridge.addListener("disambigClicked", this); bridge.addListener("issuesClicked", this); } @@ -48,10 +46,11 @@ DisambigResult[] stringArray = new DisambigResult[array.length()]; for (int i = 0; i < array.length(); i++) { // Decode the href that we got into a PageTitle, and create a DisambigResult with it - stringArray[i] = new DisambigResult(site.titleForInternalLink(array.getString(i))); + stringArray[i] = new DisambigResult(getSite().titleForInternalLink(array.getString(i))); } return stringArray; } + abstract Site getSite(); abstract int getDialogHeight(); } diff --git a/wikipedia/src/main/java/org/wikipedia/page/PageViewFragment.java b/wikipedia/src/main/java/org/wikipedia/page/PageViewFragment.java deleted file mode 100644 index 5bf5050..0000000 --- a/wikipedia/src/main/java/org/wikipedia/page/PageViewFragment.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.wikipedia.page; - -import org.wikipedia.PageTitle; -import org.wikipedia.R; -import org.wikipedia.history.HistoryEntry; -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v7.app.ActionBarActivity; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -/** - * Fragment that displays a single Page (WebView plus ToC drawer). - * This is an "empty shell" of a fragment that passes all the actual logic to an internal - * PageViewFragmentInternal class, and here's why: - * - * When a fragment is replaced by another fragment, and placed onto the backstack, the old - * fragment's View is destroyed, but the actual Fragment object remains in memory (so that - * the fragment View may be rebuilt if the user goes "back" to it, without having to use - * savedInstanceState). The problem is that our PageViewFragment is a very heavy object - * (in terms of actual bytecode), and that's what eats up a lot of memory when multiple - * PageViewFragments are stacked on top of each other. - * - * So, I've separated all the "heavy" code into a PageViewFragmentInternal object, and made - * an extremely light PageViewFragment object. The heavy object has all the same methods expected - * from a Fragment, and becomes a puppet of the light object. The crucial point comes when the - * fragment's View is destroyed: the light object sets its instance of the heavy object to null, - * thus freeing all of that memory. - * - * The light fragment maintains a set of arguments, so that the heavy object can be recreated - * if the user goes back to it. - */ -public class PageViewFragment extends Fragment { - private static final String KEY_TITLE = "title"; - private static final String KEY_CURRENT_HISTORY_ENTRY = "currentHistoryEntry"; - private static final String KEY_SCROLL_Y = "scrollY"; - - private PageViewFragmentInternal fragment; - - /** - * Access the underlying PageViewFragmentInternal object, for interacting with its - * WebView and page manipulation functions. - * @return Underlying object responsible for rendering the page. May be null if this - * fragment's View has been destroyed. - */ - public PageViewFragmentInternal getFragment() { - return fragment; - } - - /** - * Factory method for creating new instances of the fragment. - * @param title Title of the page to be displayed. - * @param entry HistoryEntry associated with this page. - * @return New instance of this fragment. - */ - public static PageViewFragment newInstance(PageTitle title, HistoryEntry entry) { - PageViewFragment f = new PageViewFragment(); - Bundle args = new Bundle(); - args.putParcelable(KEY_TITLE, title); - args.putParcelable(KEY_CURRENT_HISTORY_ENTRY, entry); - args.putInt(KEY_SCROLL_Y, 0); - f.setArguments(args); - return f; - } - - public PageViewFragment() { - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - fragment = new PageViewFragmentInternal(this, - (PageTitle)getArguments().getParcelable(KEY_TITLE), - (HistoryEntry)getArguments().getParcelable(KEY_CURRENT_HISTORY_ENTRY), - getArguments().getInt(KEY_SCROLL_Y)); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, final Bundle savedInstanceState) { - if (fragment == null) { - //this implies that we previously disposed of our internal fragment object, and - //now need to recreate it, based on the state that we kept. - fragment = new PageViewFragmentInternal(this, - (PageTitle)getArguments().getParcelable(KEY_TITLE), - (HistoryEntry)getArguments().getParcelable(KEY_CURRENT_HISTORY_ENTRY), - getArguments().getInt(KEY_SCROLL_Y)); - } - return fragment.onCreateView(inflater, container, savedInstanceState); - } - - @Override - public void onResume() { - super.onResume(); - ((ActionBarActivity)getActivity()).getSupportActionBar().setTitle(""); - } - - @Override - public void onDestroyView() { - if (fragment != null) { - // update our arguments, so that we'll be able to precisely recreate - // the internal object based on them. - getArguments().putInt(KEY_SCROLL_Y, fragment.getScrollY()); - - // let the internal object do any cleanup of its own. - fragment.onDestroyView(); - - // the key to everything: dispose of the internal object, thus freeing a whole bunch of memory, - // and keeping just a minimal amount of data on the back-stack. - fragment = null; - } - super.onDestroyView(); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - if (fragment == null) { - return; - } - fragment.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (fragment == null) { - return; - } - fragment.onActivityResult(requestCode, resultCode, data); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (!isAdded() || ((PageActivity)getActivity()).isSearching()) { - return; - } - inflater.inflate(R.menu.menu_page_actions, menu); - } - - @Override - public void onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - if (!isAdded() || ((PageActivity)getActivity()).isSearching()) { - return; - } - fragment.onPrepareOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (fragment.onOptionsItemSelected(item)) { - return true; - } else { - return super.onOptionsItemSelected(item); - } - } -} diff --git a/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java b/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java index 98f4372..608d5af 100644 --- a/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java +++ b/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java @@ -31,22 +31,22 @@ import org.wikipedia.search.SearchBarHideHandler; import org.wikipedia.views.DisableableDrawerLayout; import org.wikipedia.views.ObservableWebView; +import org.wikipedia.views.SwipeRefreshLayoutWithScroll; import org.mediawiki.api.json.Api; import org.mediawiki.api.json.ApiException; import org.mediawiki.api.json.ApiResult; import org.mediawiki.api.json.RequestBuilder; import org.json.JSONException; import org.json.JSONObject; -import org.wikipedia.views.SwipeRefreshLayoutWithScroll; - import android.app.AlertDialog; import android.content.Intent; -import android.content.res.Resources; import android.graphics.Bitmap; import android.os.Build; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.app.ActionBarActivity; import android.support.v7.view.ActionMode; import android.text.Html; import android.util.Log; @@ -54,6 +54,7 @@ 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; @@ -68,19 +69,9 @@ import java.util.Locale; import java.util.Map; -/** - * "Fragment" that displays a single Page (WebView plus ToC drawer). - * Notice that this class has all the methods of a Fragment, but does not actually derive - * from Fragment. This class is instantiated, and becomes a puppet of, the PageViewFragment - * class, which does in fact derive from Fragment. - * - * This class handles all of the heavy logic of displaying and interacting with a Page, but - * is also designed to be fully disposable when no longer needed. This allows a large number - * of PageViewFragments to be in the backstack of the FragmentManager, without significantly - * impacting memory usage. - */ -public class PageViewFragmentInternal implements BackPressedHandler { - private static final String TAG = "PageViewFragmentInt"; + +public class PageViewFragmentInternal extends Fragment implements BackPressedHandler { + private static final String TAG = "PageViewFragment"; public static final int STATE_NO_FETCH = 1; public static final int STATE_INITIAL_FETCH = 2; @@ -92,6 +83,13 @@ private int state = STATE_NO_FETCH; private int subState = SUBSTATE_NONE; + + /** + * List of lightweight history items to serve as the backstack for this fragment. + * Since the list consists of Parcelable objects, it can be saved and restored from the + * savedInstanceState of the fragment. + */ + private ArrayList<PageBackStackItem> backStack; /** * Whether to save the full page content as soon as it's loaded. @@ -110,7 +108,11 @@ */ private boolean cacheOnComplete = true; - private PageViewFragment parentFragment; + /** + * Sequence number to maintain synchronization when loading page content asynchronously + * between the Java and Javascript layers, as well as between async tasks and the UI thread. + */ + private int pageSequenceNum; private PageTitle title; private PageTitle titleOriginal; @@ -138,41 +140,14 @@ private WikipediaApp app; - private int scrollY; - public int getScrollY() { - return webView.getScrollY(); - } + /** + * The y-offset position to which the page will be scrolled once it's fully loaded + * (or loaded to the point where it can be scrolled to the correct position). + */ + private int stagedScrollY; private SavedPagesFunnel savedPagesFunnel; private ConnectionIssueFunnel connectionIssueFunnel; - - public PageViewFragmentInternal(PageViewFragment parentFragment, PageTitle title, HistoryEntry historyEntry, int scrollY) { - this.parentFragment = parentFragment; - this.title = title; - this.curEntry = historyEntry; - this.scrollY = scrollY; - } - - public PageActivity getActivity() { - return (PageActivity)parentFragment.getActivity(); - } - - public boolean isAdded() { - return parentFragment.isAdded(); - } - - private Resources getResources() { - return parentFragment.getResources(); - } - - private String getString(int resId) { - return parentFragment.getString(resId); - } - - private String getString(int resId, Object... formatArgs) { - return parentFragment.getString(resId, formatArgs); - } - public ObservableWebView getWebView() { return webView; @@ -190,19 +165,25 @@ return curEntry; } + public PageViewFragmentInternal() { + backStack = new ArrayList<>(); + } + private void displayLeadSection() { try { JSONObject marginPayload = new JSONObject(); - int margin = (int)(getResources().getDimension(R.dimen.activity_horizontal_margin) - / getResources().getDisplayMetrics().density); + int margin = (int) (getResources().getDimension(R.dimen.activity_horizontal_margin) + / getResources().getDisplayMetrics().density); marginPayload.put("marginLeft", margin); marginPayload.put("marginRight", margin); bridge.sendMessage("setMargins", marginPayload); JSONObject leadSectionPayload = new JSONObject(); + leadSectionPayload.put("sequence", pageSequenceNum); leadSectionPayload.put("title", page.getDisplayTitle()); leadSectionPayload.put("section", page.getSections().get(0).toJSON()); - leadSectionPayload.put("string_page_similar_titles", getString(R.string.page_similar_titles)); + leadSectionPayload + .put("string_page_similar_titles", getString(R.string.page_similar_titles)); leadSectionPayload.put("string_page_issues", getString(R.string.button_page_issues)); leadSectionPayload.put("string_table_infobox", getString(R.string.table_infobox)); leadSectionPayload.put("string_table_other", getString(R.string.table_other)); @@ -214,15 +195,17 @@ leadSectionPayload.put("apiLevel", Build.VERSION.SDK_INT); bridge.sendMessage("displayLeadSection", leadSectionPayload); - Utils.setupDirectionality(title.getSite().getLanguage(), Locale.getDefault().getLanguage(), bridge); + Utils.setupDirectionality(title.getSite().getLanguage(), + Locale.getDefault().getLanguage(), bridge); // Hide edit pencils if anon editing is disabled by remote killswitch or if this is a file page JSONObject miscPayload = new JSONObject(); - boolean isAnonEditingDisabled = app.getRemoteConfig().getConfig().optBoolean("disableAnonEditing", false) - && !app.getUserInfoStorage().isLoggedIn(); + boolean isAnonEditingDisabled = app.getRemoteConfig().getConfig() + .optBoolean("disableAnonEditing", false) + && !app.getUserInfoStorage().isLoggedIn(); miscPayload.put("noedit", (isAnonEditingDisabled - || title.isFilePage() - || page.getPageProperties().isMainPage())); + || title.isFilePage() + || page.getPageProperties().isMainPage())); miscPayload.put("protect", !page.getPageProperties().canEdit()); bridge.sendMessage("setPageProtected", miscPayload); } catch (JSONException e) { @@ -235,21 +218,25 @@ } refreshView.setRefreshing(false); - getActivity().updateProgressBar(true, true, 0); + ((PageActivity) getActivity()).updateProgressBar(true, true, 0); } private void displayNonLeadSection(int index) { - getActivity().updateProgressBar(true, false, PageActivity.PROGRESS_BAR_MAX_VALUE - / page.getSections().size() * index); + ((PageActivity) getActivity()).updateProgressBar(true, false, + PageActivity.PROGRESS_BAR_MAX_VALUE / page + .getSections().size() * index); try { JSONObject wrapper = new JSONObject(); + wrapper.put("sequence", pageSequenceNum); if (index < page.getSections().size()) { wrapper.put("section", page.getSections().get(index).toJSON()); wrapper.put("index", index); - if (sectionTargetFromIntent > 0 && sectionTargetFromIntent < page.getSections().size()) { + if (sectionTargetFromIntent > 0 && sectionTargetFromIntent < page.getSections() + .size()) { //if we have a section to scroll to (from our Intent): - wrapper.put("fragment", page.getSections().get(sectionTargetFromIntent).getAnchor()); + wrapper.put("fragment", + page.getSections().get(sectionTargetFromIntent).getAnchor()); } else if (sectionTargetFromTitle != null) { //if we have a section to scroll to (from our PageTitle): wrapper.put("fragment", sectionTargetFromTitle); @@ -258,22 +245,25 @@ wrapper.put("noMore", true); } //give it our expected scroll position, in case we need the page to be pre-scrolled upon loading. - wrapper.put("scrollY", (int)(scrollY / getResources().getDisplayMetrics().density)); + wrapper.put("scrollY", + (int) (stagedScrollY / getResources().getDisplayMetrics().density)); bridge.sendMessage("displaySection", wrapper); } catch (JSONException e) { //nope } } - public View onCreateView(LayoutInflater inflater, ViewGroup container, final Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_page, container, false); + public View onCreateView(LayoutInflater inflater, ViewGroup container, + final Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_page, container, false); webView = (ObservableWebView) rootView.findViewById(R.id.page_web_view); networkError = rootView.findViewById(R.id.page_error); retryButton = rootView.findViewById(R.id.page_error_retry); pageDoesNotExistError = rootView.findViewById(R.id.page_does_not_exist); tocDrawer = (DisableableDrawerLayout) rootView.findViewById(R.id.page_toc_drawer); - refreshView = (SwipeRefreshLayoutWithScroll) rootView.findViewById(R.id.page_refresh_container); + refreshView = (SwipeRefreshLayoutWithScroll) rootView + .findViewById(R.id.page_refresh_container); int swipeOffset = Utils.getActionBarSize(getActivity()); refreshView.setProgressViewOffset(true, -swipeOffset, swipeOffset); refreshView.setSize(SwipeRefreshLayout.LARGE); @@ -304,35 +294,30 @@ public void onDestroyView() { //uninitialize the bridge, so that no further JS events can have any effect. bridge.cleanup(); + super.onDestroyView(); } public void onActivityCreated(Bundle savedInstanceState) { - if (title == null) { - throw new RuntimeException("No PageTitle passed in to constructor or in instanceState"); - } - titleOriginal = title; - - //save any section-specific link target from the title, since the title may be - //replaced (normalized) - sectionTargetFromTitle = title.getFragment(); - - app = (WikipediaApp)getActivity().getApplicationContext(); - - savedPagesFunnel = app.getFunnelManager().getSavedPagesFunnel(title.getSite()); - + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(true); + app = (WikipediaApp) getActivity().getApplicationContext(); connectionIssueFunnel = new ConnectionIssueFunnel(app); + + if (savedInstanceState != null) { + backStack = savedInstanceState.getParcelableArrayList("backStack"); + } updateFontSize(); // Explicitly set background color of the WebView (independently of CSS, because // the background may be shown momentarily while the WebView loads content, // creating a seizure-inducing effect, or at the very least, a migraine with aura). - webView.setBackgroundColor(getResources().getColor(Utils.getThemedAttributeId(getActivity(), R.attr.page_background_color))); + webView.setBackgroundColor(getResources().getColor( + Utils.getThemedAttributeId(getActivity(), R.attr.page_background_color))); bridge = new CommunicationBridge(webView, "file:///android_asset/index.html"); setupMessageHandlers(); - Utils.setupDirectionality(title.getSite().getLanguage(), Locale.getDefault().getLanguage(), bridge); linkHandler = new LinkHandler(getActivity(), bridge) { @Override public void onPageLinkClicked(String anchor) { @@ -356,8 +341,9 @@ if (referenceDialog != null && referenceDialog.isShowing()) { referenceDialog.dismiss(); } - HistoryEntry historyEntry = new HistoryEntry(title, HistoryEntry.SOURCE_INTERNAL_LINK); - getActivity().displayNewPage(title, historyEntry); + HistoryEntry historyEntry = new HistoryEntry(title, + HistoryEntry.SOURCE_INTERNAL_LINK); + ((PageActivity) getActivity()).displayNewPage(title, historyEntry); } @Override @@ -370,7 +356,8 @@ @Override protected void onReferenceClicked(String refHtml) { if (!isAdded()) { - Log.d("PageViewFragment", "Detached from activity, so stopping reference click."); + Log.d("PageViewFragment", + "Detached from activity, so stopping reference click."); return; } @@ -382,7 +369,12 @@ } }; - new PageInfoHandler(getActivity(), bridge, title.getSite()) { + new PageInfoHandler(((PageActivity) getActivity()), bridge) { + @Override + Site getSite() { + return title.getSite(); + } + @Override int getDialogHeight() { // could have scrolled up a bit but the page info links must still be visible else they couldn't have been clicked @@ -390,7 +382,8 @@ } }; - bridge.injectStyleBundle(app.getStyleLoader().getAvailableBundle(StyleLoader.BUNDLE_PAGEVIEW)); + bridge.injectStyleBundle( + app.getStyleLoader().getAvailableBundle(StyleLoader.BUNDLE_PAGEVIEW)); if (app.getCurrentTheme() == WikipediaApp.THEME_DARK) { new NightModeHandler(bridge).turnOn(true); @@ -403,91 +396,224 @@ ViewAnimations.fadeOut(networkError, new Runnable() { @Override public void run() { - performActionForState(state); + displayNewPage(titleOriginal, curEntry, true, false); retryButton.setEnabled(true); } }); } }); - editHandler = new EditHandler(parentFragment, bridge); + editHandler = new EditHandler(this, bridge); + tocHandler = new ToCHandler(((PageActivity) getActivity()), tocDrawer, bridge); - imagesContainer = (ViewGroup) parentFragment.getView().findViewById(R.id.page_images_container); - leadImagesHandler = new LeadImagesHandler(getActivity(), this, bridge, webView, imagesContainer); - searchBarHideHandler = getActivity().getSearchBarHideHandler(); + imagesContainer = (ViewGroup) getView().findViewById(R.id.page_images_container); + leadImagesHandler = new LeadImagesHandler(getActivity(), this, bridge, webView, + imagesContainer); + searchBarHideHandler = ((PageActivity) getActivity()).getSearchBarHideHandler(); searchBarHideHandler.setScrollView(webView); // TODO: remove this A/B toggle when we know which one we want to keep. // (and when ready to release to production) if (BottomContentHandler.useNewBottomContent(app)) { bottomContentHandler = new BottomContentHandler(this, bridge, webView, linkHandler, - (ViewGroup) parentFragment.getView() - .findViewById(R.id.bottom_content_container), title); + (ViewGroup) getView().findViewById( + R.id.bottom_content_container)); } else { bottomContentHandler = new BottomContentHandlerOld(this, bridge, webView, linkHandler, - (ViewGroup) parentFragment.getView() - .findViewById(R.id.bottom_content_container), title); + (ViewGroup) getView().findViewById( + R.id.bottom_content_container)); } - if (tocHandler == null) { - tocHandler = new ToCHandler(getActivity(), - tocDrawer, - bridge, - title.getSite(), - isFirstPage()); - } + pageSequenceNum = 0; - if (savedInstanceState == null && curEntry.getSource() == HistoryEntry.SOURCE_MAIN_PAGE) { - // if we're asked to load the main page, and we're not coming back from a screen - // rotation or recreated activity (savedInstanceState), then do NOT load it from - // cache. - state = STATE_NO_FETCH; - cacheOnComplete = true; - setState(state); - performActionForState(state); - } else { - loadPageFromCache(); + // if we already have pages in the backstack (whether it's from savedInstanceState, or + // from being stored in the activity's fragment backstack), then load the topmost page + // on the backstack. + if (backStack.size() > 0) { + loadPageFromBackStack(); } } - private void loadPageFromCache() { - getActivity().updateProgressBar(true, true, 0); - //is this page in cache?? - app.getPageCache().get(titleOriginal, new PageCache.CacheGetListener() { - @Override - public void onGetComplete(Page page) { - if (page != null) { - Log.d(TAG, "Using page from cache: " + titleOriginal.getDisplayText()); - PageViewFragmentInternal.this.page = page; - title = page.getTitle(); - state = STATE_COMPLETE_FETCH; - // don't re-cache the page after loading. - cacheOnComplete = false; - } else { - // page isn't in cache, so fetch it from the network... - state = STATE_NO_FETCH; - cacheOnComplete = true; - } - setState(state); - performActionForState(state); - } + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // update the topmost entry in the backstack + updateBackStackItem(); + outState.putParcelableArrayList("backStack", backStack); + } - @Override - public void onGetError(Throwable e) { - Log.e(TAG, "Failed to get page from cache.", e); - // something failed when loading it from cache, so fetch it from network... - state = STATE_NO_FETCH; - // and make sure to write it to cache when it's loaded. - cacheOnComplete = true; - setState(state); - performActionForState(state); - } - }); + @Override + public void onResume() { + super.onResume(); + ((ActionBarActivity) getActivity()).getSupportActionBar().setTitle(""); + } + + /** + * Pop the topmost entry from the backstack. + * Does NOT automatically load the next topmost page on the backstack. + */ + private void popBackStack() { + if (backStack.size() == 0) { + return; + } + backStack.remove(backStack.size() - 1); + } + + /** + * Push the current page title onto the backstack. + */ + private void pushBackStack() { + PageBackStackItem item = new PageBackStackItem(titleOriginal, curEntry); + backStack.add(item); + } + + /** + * Update the current topmost backstack item, based on the currently displayed page. + * (Things like the last y-offset position should be updated here) + * Should be done right before loading a new page. + */ + private void updateBackStackItem() { + if (backStack.size() == 0) { + return; + } + PageBackStackItem item = backStack.get(backStack.size() - 1); + item.setScrollY(webView.getScrollY()); + } + + private void loadPageFromBackStack() { + if (backStack.size() == 0) { + return; + } + PageBackStackItem item = backStack.get(backStack.size() - 1); + // display the page based on the backstack item... + displayNewPage(item.getTitle(), item.getHistoryEntry(), true, false); + // and stage the scrollY position based on the backstack item. + stagedScrollY = item.getScrollY(); + } + + /** + * Load a new page into the WebView in this fragment. + * This shall be the single point of entry for loading content into the WebView, whether it's + * loading an entirely new page, refreshing the current page, retrying a failed network + * request, etc. + * @param title Title of the new page to load. + * @param entry HistoryEntry associated with the new page. + * @param tryFromCache Whether to try loading the page from cache (otherwise load directly + * from network). + * @param pushBackStack Whether to push the new page onto the backstack. + */ + public void displayNewPage(PageTitle title, HistoryEntry entry, boolean tryFromCache, + boolean pushBackStack) { + if (pushBackStack) { + // update the topmost entry in the backstack, before we start overwriting things. + updateBackStackItem(); + } + + networkError.setVisibility(View.GONE); + + state = STATE_NO_FETCH; + subState = SUBSTATE_NONE; + + this.title = title; + this.curEntry = entry; + titleOriginal = title; + savedPagesFunnel = app.getFunnelManager().getSavedPagesFunnel(title.getSite()); + + if (pushBackStack) { + pushBackStack(); + } + + // increment our sequence number, so that any async tasks that depend on the sequence + // will invalidate themselves upon completion. + pageSequenceNum++; + + // kick off an event to the WebView that will cause it to clear its contents, + // and then report back to us when the clearing is complete, so that we can synchronize + // the transitions of our native components to the new page content. + // The callback event from the WebView will then call the loadPageOnWebViewReady() + // function, which will continue the loading process. + ((PageActivity) getActivity()).updateProgressBar(true, true, 0); + try { + JSONObject wrapper = new JSONObject(); + // whatever we pass to this event will be passed back to us by the WebView! + wrapper.put("sequence", pageSequenceNum); + wrapper.put("tryFromCache", tryFromCache); + bridge.sendMessage("beginNewPage", wrapper); + } catch (JSONException e) { + //nope + } + } + + private void loadPageOnWebViewReady(boolean tryFromCache) { + // reset staged scroll position to the top. + stagedScrollY = 0; + + // stage any section-specific link target from the title, since the title may be + // replaced (normalized) + sectionTargetFromTitle = title.getFragment(); + + Utils.setupDirectionality(title.getSite().getLanguage(), Locale.getDefault().getLanguage(), + bridge); + + // hide the native top and bottom components... + leadImagesHandler.hide(); + bottomContentHandler.hide(); + bottomContentHandler.setTitle(title); + + if (curEntry.getSource() == HistoryEntry.SOURCE_SAVED_PAGE) { + state = STATE_NO_FETCH; + loadSavedPage(); + } else if (tryFromCache) { + //is this page in cache?? + app.getPageCache() + .get(titleOriginal, pageSequenceNum, new PageCache.CacheGetListener() { + @Override + public void onGetComplete(Page page, int sequence) { + if (sequence != pageSequenceNum) { + return; + } + if (page != null) { + Log.d(TAG, "Using page from cache: " + titleOriginal.getDisplayText()); + PageViewFragmentInternal.this.page = page; + PageViewFragmentInternal.this.title = page.getTitle(); + // Save history entry... + new SaveHistoryTask(curEntry).execute(); + // don't re-cache the page after loading. + cacheOnComplete = false; + state = STATE_COMPLETE_FETCH; + setState(state); + performActionForState(state); + } else { + // page isn't in cache, so fetch it from the network... + loadPageFromNetwork(); + } + } + + @Override + public void onGetError(Throwable e, int sequence) { + Log.e(TAG, "Failed to get page from cache.", e); + if (sequence != pageSequenceNum) { + return; + } + // something failed when loading it from cache, so fetch it from network... + loadPageFromNetwork(); + } + }); + } else { + loadPageFromNetwork(); + } + } + + private void loadPageFromNetwork() { + state = STATE_NO_FETCH; + // and make sure to write it to cache when it's loaded. + cacheOnComplete = true; + setState(state); + performActionForState(state); } private boolean isFirstPage() { - return parentFragment.getFragmentManager().getBackStackEntryCount() == 0 - && !webView.canGoBack(); + return backStack.size() <= 1 && !webView.canGoBack(); } public Bitmap getLeadImageBitmap() { @@ -511,6 +637,22 @@ } private void setupMessageHandlers() { + bridge.addListener("onBeginNewPage", new CommunicationBridge.JSEventListener() { + @Override + public void onMessage(String messageType, JSONObject messagePayload) { + if (!isAdded()) { + return; + } + try { + if (messagePayload.getInt("sequence") != pageSequenceNum) { + return; + } + loadPageOnWebViewReady(messagePayload.getBoolean("tryFromCache")); + } catch (JSONException e) { + //nope + } + } + }); bridge.addListener("requestSection", new CommunicationBridge.JSEventListener() { @Override public void onMessage(String messageType, JSONObject messagePayload) { @@ -518,6 +660,9 @@ return; } try { + if (messagePayload.getInt("sequence") != pageSequenceNum) { + return; + } displayNonLeadSection(messagePayload.getInt("index")); } catch (JSONException e) { //nope @@ -548,8 +693,15 @@ if (!isAdded()) { return; } + try { + if (messagePayload.getInt("sequence") != pageSequenceNum) { + return; + } + } catch (JSONException e) { + // nope + } // Do any other stuff that should happen upon page load completion... - getActivity().updateProgressBar(false, true, 0); + ((PageActivity) getActivity()).updateProgressBar(false, true, 0); // trigger layout of the bottom content // Check to see if the page title has changed (e.g. due to following a redirect), @@ -595,18 +747,16 @@ } public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); if (requestCode == PageActivity.ACTIVITY_REQUEST_EDIT_SECTION - && resultCode == EditHandler.RESULT_REFRESH_PAGE) { + && resultCode == EditHandler.RESULT_REFRESH_PAGE) { //Retrieve section ID from intent, and find correct section, so where know where to scroll to sectionTargetFromIntent = data.getIntExtra(EditSectionActivity.EXTRA_SECTION_ID, 0); //reset our scroll offset, since we have a section scroll target - scrollY = 0; - - hidePageContent(); + stagedScrollY = 0; // and reload the page... - setState(STATE_NO_FETCH); - performActionForState(state); + displayNewPage(titleOriginal, curEntry, false, false); } } @@ -616,21 +766,15 @@ } switch (forState) { case STATE_NO_FETCH: - getActivity().updateProgressBar(true, true, 0); - bridge.sendMessage("clearContents", new JSONObject()); - + ((PageActivity) getActivity()).updateProgressBar(true, true, 0); // hide the lead image... leadImagesHandler.hide(); - getActivity().getSearchBarHideHandler().setFadeEnabled(false); - - if (curEntry.getSource() == HistoryEntry.SOURCE_SAVED_PAGE) { - loadSavedPage(); - } else { - new LeadSectionFetchTask().execute(); - } + bottomContentHandler.hide(); + ((PageActivity) getActivity()).getSearchBarHideHandler().setFadeEnabled(false); + new LeadSectionFetchTask(pageSequenceNum).execute(); break; case STATE_INITIAL_FETCH: - new RestSectionsFetchTask().execute(); + new RestSectionsFetchTask(pageSequenceNum).execute(); break; case STATE_COMPLETE_FETCH: editHandler.setPage(page); @@ -669,7 +813,7 @@ // FIXME: Move this out into a PageComplete event of sorts if (state == STATE_COMPLETE_FETCH) { - tocHandler.setupToC(page); + tocHandler.setupToC(page, title.getSite(), isFirstPage()); //add the page to cache! if (cacheOnComplete) { @@ -684,11 +828,21 @@ } }); } - } } + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (!isAdded() || ((PageActivity)getActivity()).isSearching()) { + return; + } + inflater.inflate(R.menu.menu_page_actions, menu); + } + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + if (!isAdded() || ((PageActivity)getActivity()).isSearching()) { + return; + } MenuItem savePageMenu = menu.findItem(R.id.menu_save_page); if (savePageMenu == null) { return; @@ -700,24 +854,24 @@ MenuItem themeChooserMenu = menu.findItem(R.id.menu_themechooser); switch (state) { - case PageViewFragmentInternal.STATE_NO_FETCH: - case PageViewFragmentInternal.STATE_INITIAL_FETCH: + case STATE_NO_FETCH: + case STATE_INITIAL_FETCH: savePageMenu.setEnabled(false); shareMenu.setEnabled(false); otherLangMenu.setEnabled(false); findInPageMenu.setEnabled(false); themeChooserMenu.setEnabled(false); break; - case PageViewFragmentInternal.STATE_COMPLETE_FETCH: + case STATE_COMPLETE_FETCH: savePageMenu.setEnabled(true); shareMenu.setEnabled(true); otherLangMenu.setEnabled(true); findInPageMenu.setEnabled(true); themeChooserMenu.setEnabled(true); - if (subState == PageViewFragmentInternal.SUBSTATE_PAGE_SAVED) { + if (subState == SUBSTATE_PAGE_SAVED) { savePageMenu.setEnabled(false); savePageMenu.setTitle(WikipediaApp.getInstance().getString(R.string.menu_page_saved)); - } else if (subState == PageViewFragmentInternal.SUBSTATE_SAVED_PAGE_LOADED) { + } else if (subState == SUBSTATE_SAVED_PAGE_LOADED) { savePageMenu.setTitle(WikipediaApp.getInstance().getString(R.string.menu_refresh_saved_page)); } else { savePageMenu.setTitle(WikipediaApp.getInstance().getString(R.string.menu_save_page)); @@ -749,7 +903,7 @@ } return true; case R.id.menu_share_page: - getActivity().share(); + ((PageActivity) getActivity()).share(); return true; case R.id.menu_other_languages: Intent langIntent = new Intent(); @@ -762,15 +916,15 @@ showFindInPage(); return true; case R.id.menu_themechooser: - getActivity().showThemeChooser(); + ((PageActivity) getActivity()).showThemeChooser(); return true; default: - return false; + return super.onOptionsItemSelected(item); } } public void showFindInPage() { - final PageActivity pageActivity = getActivity(); + final PageActivity pageActivity = ((PageActivity) getActivity()); final FindInPageActionProvider findInPageActionProvider = new FindInPageActionProvider(pageActivity); pageActivity.startSupportActionMode(new ActionMode.Callback() { @@ -864,8 +1018,9 @@ } private class LeadSectionFetchTask extends SectionsFetchTask { - public LeadSectionFetchTask() { + public LeadSectionFetchTask(int sequenceNum) { super(getActivity(), title, "0"); + this.sequenceNum = sequenceNum; } @Override @@ -879,10 +1034,14 @@ return builder; } + private final int sequenceNum; private PageProperties pageProperties; @Override public List<Section> processResult(ApiResult result) throws Throwable { + if (sequenceNum != pageSequenceNum) { + return super.processResult(result); + } JSONObject mobileView = result.asObject().optJSONObject("mobileview"); if (mobileView != null) { pageProperties = new PageProperties(mobileView); @@ -902,9 +1061,7 @@ @Override public void onFinish(List<Section> result) { - // have we been unwittingly detached from our Activity? - if (!isAdded()) { - Log.d("PageViewFragment", "Detached from activity, so stopping update."); + if (!isAdded() || sequenceNum != pageSequenceNum) { return; } @@ -953,20 +1110,21 @@ @Override public void onCatch(Throwable caught) { - commonSectionFetchOnCatch(caught); + commonSectionFetchOnCatch(caught, sequenceNum); } } private class RestSectionsFetchTask extends SectionsFetchTask { - public RestSectionsFetchTask() { + private final int sequenceNum; + + public RestSectionsFetchTask(int sequenceNum) { super(getActivity(), title, "1-"); + this.sequenceNum = sequenceNum; } @Override public void onFinish(List<Section> result) { - // have we been unwittingly detached from our Activity? - if (!isAdded()) { - Log.d("PageViewFragment", "Detached from activity, so stopping update."); + if (!isAdded() || sequenceNum != pageSequenceNum) { return; } ArrayList<Section> newSections = (ArrayList<Section>) page.getSections().clone(); @@ -985,17 +1143,17 @@ @Override public void onCatch(Throwable caught) { - commonSectionFetchOnCatch(caught); + commonSectionFetchOnCatch(caught, sequenceNum); } } - private void commonSectionFetchOnCatch(Throwable caught) { - if (!isAdded()) { + private void commonSectionFetchOnCatch(Throwable caught, int sequenceNum) { + if (!isAdded() || sequenceNum != pageSequenceNum) { return; } // in any case, make sure the TOC drawer is closed tocDrawer.closeDrawers(); - getActivity().updateProgressBar(false, true, 0); + ((PageActivity) getActivity()).updateProgressBar(false, true, 0); refreshView.setRefreshing(false); if (caught instanceof SectionsFetchException) { @@ -1152,14 +1310,11 @@ public void refreshPage(boolean saveOnComplete) { this.saveOnComplete = saveOnComplete; - cacheOnComplete = true; if (saveOnComplete) { Toast.makeText(getActivity(), R.string.toast_refresh_saved_page, Toast.LENGTH_LONG).show(); } - hidePageContent(); curEntry = new HistoryEntry(title, HistoryEntry.SOURCE_HISTORY); - setState(STATE_NO_FETCH); - performActionForState(state); + displayNewPage(title, curEntry, false, false); } public static final int TOC_ACTION_SHOW = 0; @@ -1199,6 +1354,11 @@ if (closeFindInPage()) { return true; } + if (backStack.size() > 1) { + popBackStack(); + loadPageFromBackStack(); + return true; + } return false; } } diff --git a/wikipedia/src/main/java/org/wikipedia/page/ToCHandler.java b/wikipedia/src/main/java/org/wikipedia/page/ToCHandler.java index 8526603..2df4878 100644 --- a/wikipedia/src/main/java/org/wikipedia/page/ToCHandler.java +++ b/wikipedia/src/main/java/org/wikipedia/page/ToCHandler.java @@ -39,14 +39,9 @@ private final CommunicationBridge bridge; private final DisableableDrawerLayout slidingPane; private final TextView headerView; - private final ToCInteractionFunnel funnel; private final ActionBarActivity parentActivity; + private ToCInteractionFunnel funnel; - /** - * We don't want to open the ToC drawer on the first page loaded automatically - * so the back button sends users back to the previous activity. - */ - private final boolean firstPage; /** * 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 @@ -56,13 +51,10 @@ private boolean openedViaSwipe = true; public ToCHandler(final ActionBarActivity activity, final DisableableDrawerLayout slidingPane, - final CommunicationBridge bridge, final Site site, boolean firstPage) { + final CommunicationBridge bridge) { this.parentActivity = activity; this.bridge = bridge; this.slidingPane = slidingPane; - this.firstPage = firstPage; - - funnel = new ToCInteractionFunnel((WikipediaApp)slidingPane.getContext().getApplicationContext(), site); this.tocList = (ListView) slidingPane.findViewById(R.id.page_toc_list); this.tocProgress = (ProgressBar) slidingPane.findViewById(R.id.page_toc_in_progress); @@ -193,7 +185,9 @@ } } - public void setupToC(final Page page) { + public void setupToC(final Page page, Site site, boolean firstPage) { + funnel = new ToCInteractionFunnel((WikipediaApp)slidingPane.getContext().getApplicationContext(), site); + tocProgress.setVisibility(View.GONE); tocList.setVisibility(View.VISIBLE); diff --git a/wikipedia/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandler.java b/wikipedia/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandler.java index c4ebd80..f55caba 100644 --- a/wikipedia/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandler.java +++ b/wikipedia/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandler.java @@ -46,7 +46,6 @@ 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; @@ -62,18 +61,18 @@ private ImageView imagePlaceholder; private ImageViewWithFace image1; + private PageTitle pageTitle; private SuggestedPagesFunnel funnel; private SearchResults readMoreItems; public BottomContentHandler(PageViewFragmentInternal parent, CommunicationBridge bridge, ObservableWebView webview, LinkHandler linkHandler, - ViewGroup hidingView, final PageTitle pageTitle) { + ViewGroup hidingView) { this.parentFragment = parent; this.bridge = bridge; this.webView = webview; this.linkHandler = linkHandler; - this.pageTitle = pageTitle; - activity = parentFragment.getActivity(); + activity = ((PageActivity) parentFragment.getActivity()); app = (WikipediaApp) activity.getApplicationContext(); displayDensity = activity.getResources().getDisplayMetrics().density; @@ -89,8 +88,6 @@ imagePlaceholder = (ImageView) bottomContentContainer.findViewById(R.id.read_next_image_placeholder); image1 = (ImageViewWithFace) bottomContentContainer.findViewById(R.id.read_next_image_1); image1.setOnImageLoadListener(this); - - funnel = new SuggestedPagesFunnel(app, pageTitle.getSite(), 1); webview.addOnClickListener(new ObservableWebView.OnClickListener() { @Override @@ -309,6 +306,7 @@ public void setTitle(PageTitle newTitle) { pageTitle = newTitle; + funnel = new SuggestedPagesFunnel(app, pageTitle.getSite(), 1); } private void setupReadNextSection(final SearchResults results) { diff --git a/wikipedia/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandlerOld.java b/wikipedia/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandlerOld.java index 1240370..a7f6dcd 100644 --- a/wikipedia/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandlerOld.java +++ b/wikipedia/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandlerOld.java @@ -64,14 +64,13 @@ public BottomContentHandlerOld(PageViewFragmentInternal parentFragment, CommunicationBridge bridge, ObservableWebView webview, - LinkHandler linkHandler, ViewGroup hidingView, - PageTitle pageTitle) { + LinkHandler linkHandler, ViewGroup hidingView) { this.parentFragment = parentFragment; this.bridge = bridge; this.webView = webview; this.linkHandler = linkHandler; this.pageTitle = pageTitle; - activity = parentFragment.getActivity(); + activity = (PageActivity) parentFragment.getActivity(); app = (WikipediaApp) activity.getApplicationContext(); displayDensity = activity.getResources().getDisplayMetrics().density; @@ -135,8 +134,6 @@ return false; } }); - - funnel = new SuggestedPagesFunnel(app, pageTitle.getSite(), 0); // preload the display density, since it will be used in a lot of places displayDensity = activity.getResources().getDisplayMetrics().density; @@ -333,6 +330,7 @@ public void setTitle(PageTitle newTitle) { pageTitle = newTitle; + funnel = new SuggestedPagesFunnel(app, pageTitle.getSite(), 0); } private void setupReadMoreSection(LayoutInflater layoutInflater, final SearchResults results) { diff --git a/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryActivity.java b/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryActivity.java index 2266079..8bc7ead 100644 --- a/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryActivity.java +++ b/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryActivity.java @@ -210,9 +210,9 @@ updateProgressBar(false, true, 0); // find our Page in the page cache... - app.getPageCache().get(pageTitle, new PageCache.CacheGetListener() { + app.getPageCache().get(pageTitle, 0, new PageCache.CacheGetListener() { @Override - public void onGetComplete(Page page) { + public void onGetComplete(Page page, int sequence) { GalleryActivity.this.page = page; if (page != null && page.getGalleryCollection() != null && page.getGalleryCollection().getItemList().size() > 0) { @@ -226,7 +226,7 @@ } @Override - public void onGetError(Throwable e) { + public void onGetError(Throwable e, int sequence) { Log.e(TAG, "Failed to get page from cache.", e); fetchGalleryCollection(); cacheOnLoad = true; diff --git a/wikipedia/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java b/wikipedia/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java index b017205..9a85d73 100644 --- a/wikipedia/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java +++ b/wikipedia/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java @@ -109,7 +109,7 @@ } public LeadImagesHandler(final Context context, final PageViewFragmentInternal parentFragment, - CommunicationBridge bridge, final ObservableWebView webview, + CommunicationBridge bridge, ObservableWebView webview, ViewGroup hidingView) { this.context = context; this.parentFragment = parentFragment; @@ -143,7 +143,7 @@ public void onClick(float x, float y) { // if the click event is within the area of the lead image, then the user // must have wanted to click on the lead image! - if (leadImagesEnabled && y < imageContainer.getHeight() - webview.getScrollY()) { + if (leadImagesEnabled && y < imageContainer.getHeight() - webView.getScrollY()) { String imageName = parentFragment.getPage().getPageProperties() .getLeadImageName(); if (imageName != null) { @@ -431,6 +431,7 @@ titleContainerHeight = (int)(Utils.getActionBarSize(parentFragment.getActivity()) / displayDensity); // hide everything image1.setVisibility(View.GONE); + image1.setImageDrawable(null); imagePlaceholder.setVisibility(View.GONE); pageTitleText.setVisibility(View.GONE); pageDescriptionText.setVisibility(View.GONE); @@ -444,19 +445,24 @@ (int) ((titleContainerHeight) * displayDensity))); // hide the lead image image1.setVisibility(View.GONE); + image1.setImageDrawable(null); imagePlaceholder.setVisibility(View.GONE); + pageTitleText.setVisibility(View.VISIBLE); + pageDescriptionText.setVisibility(View.INVISIBLE); // set the color of the title pageTitleText.setTextColor(context.getResources() .getColor(Utils.getThemedAttributeId(parentFragment.getActivity(), R.attr.lead_disabled_text_color))); // remove bottom padding from the description pageDescriptionText.setPadding(pageDescriptionText.getPaddingLeft(), - pageDescriptionText.getPaddingTop(), pageDescriptionText.getPaddingRight(), 0); + pageDescriptionText.getPaddingTop(), + pageDescriptionText.getPaddingRight(), 0); // and give it no drop shadow pageTitleText.setShadowLayer(0, 0, 0, 0); // do the same for the description... pageDescriptionText.setTextColor(context.getResources() - .getColor(Utils.getThemedAttributeId(parentFragment.getActivity(), R.attr.lead_disabled_text_color))); + .getColor(Utils.getThemedAttributeId(parentFragment.getActivity(), + R.attr.lead_disabled_text_color))); pageDescriptionText.setShadowLayer(0, 0, 0, 0); // remove any background from the title container pageTitleContainer.setBackgroundColor(Color.TRANSPARENT); @@ -472,11 +478,17 @@ // prepare the lead image to be populated image1.setVisibility(View.INVISIBLE); imagePlaceholder.setVisibility(View.VISIBLE); - + pageTitleText.setVisibility(View.VISIBLE); + pageDescriptionText.setVisibility(View.INVISIBLE); // set the color of the title pageTitleText.setTextColor(context.getResources().getColor(R.color.lead_text_color)); final int bottomPaddingNominal = 16; titleBottomPadding = (int)(bottomPaddingNominal * displayDensity); + // give default padding to the description + pageDescriptionText.setPadding(pageDescriptionText.getPaddingLeft(), + pageDescriptionText.getPaddingTop(), + pageDescriptionText.getPaddingRight(), + titleBottomPadding); // and give it a nice drop shadow! pageTitleText.setShadowLayer(2, 1, 1, context.getResources().getColor(R.color.lead_text_shadow)); // do the same for the description... @@ -499,6 +511,11 @@ titleBottomPadding += lineSpacePadding; } } + // reset margins on the title text to default + FrameLayout.LayoutParams titleTextParams = (FrameLayout.LayoutParams) pageTitleText.getLayoutParams(); + titleTextParams.bottomMargin = 0; + pageTitleText.setLayoutParams(titleTextParams); + // and set its padding to what we calculated above pageTitleText.setPadding(pageTitleText.getPaddingLeft(), pageTitleText.getPaddingTop(), pageTitleText.getPaddingRight(), titleBottomPadding); // pad the webview contents, to account for the lead image view height that we've @@ -527,7 +544,8 @@ if (!isMainPage) { // make everything visible! - imageContainer.setVisibility(View.VISIBLE); + ViewAnimations.fadeIn(imageContainer); + //imageContainer.setVisibility(View.VISIBLE); // kick off loading of the WikiData description, if we have one if (!TextUtils.isEmpty(parentFragment.getPage().getTitle().getDescription())) { @@ -553,8 +571,10 @@ } // only show the description if it's two lines or less if (pageDescriptionText.getLineCount() > 2) { + pageDescriptionText.setVisibility(View.GONE); return; } + pageDescriptionText.setVisibility(View.INVISIBLE); final int animDuration = 500; // adjust the space between the title and the description... // for >2.3 and <5.0, the space needs to be a little different, because it doesn't diff --git a/www/js/sections.js b/www/js/sections.js index 520caf4..4ddd274 100644 --- a/www/js/sections.js +++ b/www/js/sections.js @@ -18,6 +18,17 @@ document.getElementById( "content" ).style.paddingBottom = payload.paddingBottom + "px"; }); +bridge.registerListener( "beginNewPage", function( payload ) { + clearContents(); + // fire an event back to the app, but with a slight timeout, which should + // have the effect of "waiting" until the page contents have cleared before sending the + // event, allowing synchronization of sorts between the WebView and the app. + // (If we find a better way to synchronize the two, it can be done here, as well) + setTimeout( function() { + bridge.sendMessage( "onBeginNewPage", payload ); + }, 10); +}); + bridge.registerListener( "displayLeadSection", function( payload ) { // This might be a refresh! Clear out all contents! clearContents(); @@ -138,7 +149,7 @@ scrolledOnLoad = true; } document.getElementById( "loading_sections").className = ""; - bridge.sendMessage( "pageLoadComplete", { } ); + bridge.sendMessage( "pageLoadComplete", { "sequence": payload.sequence } ); } else { var contentWrapper = document.getElementById( "content" ); elementsForSection(payload.section).forEach(function (element) { @@ -153,7 +164,7 @@ if ( typeof payload.fragment === "string" && payload.fragment.length > 0 && payload.section.anchor === payload.fragment) { scrollToSection( payload.fragment ); } - bridge.sendMessage( "requestSection", { "index": payload.section.id + 1 }); + bridge.sendMessage( "requestSection", { "sequence": payload.sequence, "index": payload.section.id + 1 }); } }); -- To view, visit https://gerrit.wikimedia.org/r/197346 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I40ac4c6f1a0351e7fec7b462d5b950c12fd09122 Gerrit-PatchSet: 15 Gerrit-Project: apps/android/wikipedia Gerrit-Branch: master Gerrit-Owner: Dbrant <dbr...@wikimedia.org> Gerrit-Reviewer: BearND <bsitzm...@wikimedia.org> Gerrit-Reviewer: Brion VIBBER <br...@wikimedia.org> Gerrit-Reviewer: Dbrant <dbr...@wikimedia.org> Gerrit-Reviewer: Deskana <dga...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits