package org.wikipedia.page.leadimages; import android.content.DialogInterface; import android.graphics.Bitmap; import android.graphics.PointF; import android.net.Uri; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.view.View; import android.view.animation.Animation; import android.view.animation.AnimationUtils; 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.GalleryFunnel; import org.wikipedia.analytics.LoginFunnel; import org.wikipedia.bridge.CommunicationBridge; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.descriptions.DescriptionEditClient; import org.wikipedia.descriptions.DescriptionEditTutorialActivity; import org.wikipedia.gallery.GalleryActivity; import org.wikipedia.login.LoginActivity; import org.wikipedia.login.User; import org.wikipedia.page.Page; import org.wikipedia.page.PageFragment; import org.wikipedia.page.PageTitle; import org.wikipedia.settings.Prefs; import org.wikipedia.util.DimenUtil; import org.wikipedia.util.StringUtil; import org.wikipedia.views.FaceAndColorDetectImageView; import org.wikipedia.views.ObservableWebView; import static org.wikipedia.settings.Prefs.isDescriptionEditTutorialEnabled; import static org.wikipedia.util.DimenUtil.getContentTopOffsetPx; public class LeadImagesHandler { /** * Minimum screen height for enabling lead images. If the screen is smaller than * this height, lead images will not be displayed, and will be substituted with just * the page title. */ private static final int MIN_SCREEN_HEIGHT_DP = 480; public interface OnLeadImageLayoutListener { void onLayoutComplete(int sequence); } @NonNull private final PageFragment parentFragment; @NonNull private final CommunicationBridge bridge; @NonNull private final ObservableWebView webView; @NonNull private final PageHeaderView pageHeaderView; private View image; private int displayHeightDp; public LeadImagesHandler(@NonNull final PageFragment parentFragment, @NonNull CommunicationBridge bridge, @NonNull ObservableWebView webView, @NonNull PageHeaderView pageHeaderView) { this.pageHeaderView = pageHeaderView; this.parentFragment = parentFragment; this.bridge = bridge; this.webView = webView; image = pageHeaderView.getImage(); initDisplayDimensions(); initWebView(); initArticleHeaderView(); // hide ourselves by default hide(); } /** * Completely hide the lead image view. Useful in case of network errors, etc. * The only way to "show" the lead image view is by calling the beginLayout function. */ public void hide() { pageHeaderView.hide(); } @Nullable public Bitmap getLeadImageBitmap() { return isLeadImageEnabled() ? pageHeaderView.copyBitmap() : null; } public boolean isLeadImageEnabled() { return WikipediaApp.getInstance().isImageDownloadEnabled() && displayHeightDp >= MIN_SCREEN_HEIGHT_DP && !TextUtils.isEmpty(getLeadImageUrl()); } public void setAnimationPaused(boolean paused) { pageHeaderView.setAnimationPaused(paused); } /** * Triggers a chain of events that will lay out the lead image, page title, and other * elements, at the end of which the WebView contents may begin to be composed. * These events (performed asynchronously) are in the following order: * - Dynamically resize the page title TextView and, if necessary, adjust its font size * based on the length of our page title. * - Dynamically resize the lead image container view and restyle it, if necessary, depending * on whether the page contains a lead image, and whether our screen resolution is high * enough to warrant a lead image. * - Send a "padding" event to the WebView so that any subsequent content that's added to it * will be correctly offset to account for the lead image view (or lack of it) * - Make the lead image view visible. * - Fire a callback to the provided Listener indicating that the rest of the WebView content * can now be loaded. * - Fetch and display the WikiData description for this page, if available. * <p/> * Realistically, the whole process will happen very quickly, and almost unnoticeably to the * user. But it still needs to be asynchronous because we're dynamically laying out views, and * because the padding "event" that we send to the WebView must come before any other content * is sent to it, so that the effect doesn't look jarring to the user. * * @param listener Listener that will receive an event when the layout is completed. */ public void beginLayout(OnLeadImageLayoutListener listener, int sequence) { if (getPage() == null) { return; } initDisplayDimensions(); // set the page title text, and honor any HTML formatting in the title loadLeadImage(); pageHeaderView.setTitle(StringUtil.fromHtml(getPage().getDisplayTitle())); pageHeaderView.setSubtitle(StringUtils.capitalize(getTitle().getDescription())); pageHeaderView.setLocale(getPage().getTitle().getWikiSite().languageCode()); pageHeaderView.setPronunciation(getPage().getTitlePronunciationUrl()); pageHeaderView.setProtected(getPage().isProtected()); pageHeaderView.setAllowDescriptionEdit(DescriptionEditClient.isEditAllowed(getPage())); layoutViews(listener, sequence); } /** * The final step in the layout process: * Apply sizing and styling to our page title and lead image views, based on how large our * page title ended up, and whether we should display the lead image. * * @param listener Listener that will receive an event when the layout is completed. */ private void layoutViews(OnLeadImageLayoutListener listener, int sequence) { if (!isFragmentAdded()) { return; } if (isMainPage()) { pageHeaderView.hide(); } else { if (!isLeadImageEnabled()) { pageHeaderView.showText(); } else { pageHeaderView.showTextImage(); } } // tell our listener that it's ok to start loading the rest of the WebView content listener.onLayoutComplete(sequence); } private void updatePadding() { int padding; if (isMainPage()) { padding = Math.round(getContentTopOffsetPx(getActivity()) / DimenUtil.getDensityScalar()); } else { padding = Math.round(pageHeaderView.getHeight() / DimenUtil.getDensityScalar()); } setWebViewPaddingTop(padding); } private void setWebViewPaddingTop(int padding) { JSONObject payload = new JSONObject(); try { payload.put("paddingTop", padding); } catch (JSONException e) { throw new RuntimeException(e); } bridge.sendMessage("setPaddingTop", payload); } /** * Determines and sets displayHeightDp for the lead images layout. */ private void initDisplayDimensions() { displayHeightDp = (int) (DimenUtil.getDisplayHeightPx() / DimenUtil.getDensityScalar()); } private void loadLeadImage() { loadLeadImage(getLeadImageUrl()); } private void loadLeadImage(@Nullable String url) { if (!isMainPage() && !TextUtils.isEmpty(url) && isLeadImageEnabled()) { pageHeaderView.loadImage(url); } else { pageHeaderView.loadImage(null); } } @Nullable private String getLeadImageUrl() { String url = getPage() == null ? null : getPage().getPageProperties().getLeadImageUrl(); if (url == null) { return null; } // Conditionally add the PageTitle's URL scheme and authority if these are missing from the // PageProperties' URL. Uri fullUri = Uri.parse(url); String scheme = getTitle().getWikiSite().scheme(); String authority = getTitle().getWikiSite().authority(); if (fullUri.getScheme() != null) { scheme = fullUri.getScheme(); } if (fullUri.getAuthority() != null) { authority = fullUri.getAuthority(); } return new Uri.Builder() .scheme(scheme) .authority(authority) .path(fullUri.getPath()) .toString(); } private void startKenBurnsAnimation() { Animation anim = AnimationUtils.loadAnimation(getActivity(), R.anim.lead_image_zoom); image.startAnimation(anim); } private void initArticleHeaderView() { pageHeaderView.setOnImageLoadListener(new ImageLoadListener()); pageHeaderView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override @SuppressWarnings("checkstyle:parameternumber") public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { updatePadding(); } }); pageHeaderView.setCallback(new PageHeaderView.Callback() { @Override public void onDescriptionClicked() { verifyDescriptionEditable(); } @Override public void onEditDescription() { verifyDescriptionEditable(); } @Override public void onEditLeadSection() { parentFragment.getEditHandler().startEditingSection(0, null); } }); } private void verifyDescriptionEditable() { if (getPage() != null && getPage().getPageProperties().canEdit()) { verifyLoggedInForDescriptionEdit(); } else { parentFragment.getEditHandler().showUneditableDialog(); } } private void verifyLoggedInForDescriptionEdit() { if (!User.isLoggedIn() && Prefs.getTotalAnonDescriptionsEdited() >= parentFragment.getResources().getInteger(R.integer.description_max_anon_edits)) { new AlertDialog.Builder(parentFragment.getContext()) .setMessage(R.string.description_edit_anon_limit) .setPositiveButton(R.string.menu_login, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { parentFragment.startActivity(LoginActivity.newIntent(parentFragment.getContext(), LoginFunnel.SOURCE_EDIT)); } }) .setNegativeButton(android.R.string.cancel, null) .show(); } else { startDescriptionEditActivity(); } } private void startDescriptionEditActivity() { if (isDescriptionEditTutorialEnabled()) { parentFragment.startActivityForResult(DescriptionEditTutorialActivity.newIntent(parentFragment.getContext()), Constants.ACTIVITY_REQUEST_DESCRIPTION_EDIT_TUTORIAL); } else { parentFragment.startDescriptionEditActivity(); } } private void initWebView() { webView.addOnScrollChangeListener(pageHeaderView); webView.addOnClickListener(new ObservableWebView.OnClickListener() { @Override public boolean 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 (getPage() != null && isLeadImageEnabled() && y < (image.getHeight() - webView.getScrollY())) { String imageName = getPage().getPageProperties().getLeadImageName(); if (imageName != null) { String filename = "File:" + imageName; WikiSite wiki = getTitle().getWikiSite(); getActivity().startActivityForResult(GalleryActivity.newIntent(getActivity(), parentFragment.getTitleOriginal(), filename, wiki, GalleryFunnel.SOURCE_LEAD_IMAGE), Constants.ACTIVITY_REQUEST_GALLERY); } return true; } return false; } }); } private boolean isMainPage() { return getPage() != null && getPage().isMainPage(); } private PageTitle getTitle() { return parentFragment.getTitle(); } @Nullable private Page getPage() { return parentFragment.getPage(); } private boolean isFragmentAdded() { return parentFragment.isAdded(); } private FragmentActivity getActivity() { return parentFragment.getActivity(); } private class ImageLoadListener implements FaceAndColorDetectImageView.OnImageLoadListener { @Override public void onImageLoaded(final int bmpHeight, @Nullable final PointF faceLocation, @ColorInt final int mainColor) { pageHeaderView.post(new Runnable() { @Override public void run() { if (isFragmentAdded()) { if (faceLocation != null) { pageHeaderView.setImageFocus(faceLocation); } startKenBurnsAnimation(); } } }); } @Override public void onImageFailed() { } } }