package org.wikipedia.page.snippet; import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.Bitmap; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.IntegerRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ImageView; import com.appenguin.onboarding.ToolTip; import org.apache.commons.lang3.StringUtils; import org.json.JSONException; import org.json.JSONObject; import org.wikipedia.R; import org.wikipedia.WikipediaApp; import org.wikipedia.activity.ActivityUtil; import org.wikipedia.analytics.ShareAFactFunnel; import org.wikipedia.bridge.CommunicationBridge; import org.wikipedia.dataclient.mwapi.MwQueryResponse; import org.wikipedia.gallery.ImageLicense; import org.wikipedia.gallery.ImageLicenseFetchClient; import org.wikipedia.page.Namespace; import org.wikipedia.page.NoDimBottomSheetDialog; import org.wikipedia.page.Page; import org.wikipedia.page.PageFragment; import org.wikipedia.page.PageProperties; import org.wikipedia.page.PageTitle; import org.wikipedia.settings.Prefs; import org.wikipedia.tooltip.ToolTipUtil; import org.wikipedia.util.FeedbackUtil; import org.wikipedia.util.ShareUtil; import org.wikipedia.util.StringUtil; import org.wikipedia.util.UriUtil; import org.wikipedia.util.log.L; import org.wikipedia.wiktionary.WiktionaryDialog; import java.util.Arrays; import retrofit2.Call; import static org.wikipedia.analytics.ShareAFactFunnel.ShareMode; /** * Let user choose between sharing as text or as image. */ public class ShareHandler { private static final String PAYLOAD_PURPOSE_KEY = "purpose"; private static final String PAYLOAD_PURPOSE_SHARE = "share"; private static final String PAYLOAD_PURPOSE_DEFINE = "define"; private static final String PAYLOAD_PURPOSE_EDIT_HERE = "edit_here"; private static final String PAYLOAD_TEXT_KEY = "text"; @ColorRes private static final int SHARE_TOOL_TIP_COLOR = R.color.foundation_blue; @NonNull private final PageFragment fragment; @NonNull private final CommunicationBridge bridge; @Nullable private CompatActionMode webViewActionMode; @Nullable private ShareAFactFunnel funnel; private void createFunnel() { WikipediaApp app = WikipediaApp.getInstance(); final Page page = fragment.getPage(); final PageProperties pageProperties = page.getPageProperties(); funnel = new ShareAFactFunnel(app, page.getTitle(), pageProperties.getPageId(), pageProperties.getRevisionId()); } public ShareHandler(@NonNull PageFragment fragment, @NonNull CommunicationBridge bridge) { this.fragment = fragment; this.bridge = bridge; bridge.addListener("onGetTextSelection", new CommunicationBridge.JSEventListener() { @Override public void onMessage(String messageType, JSONObject messagePayload) { String purpose = messagePayload.optString(PAYLOAD_PURPOSE_KEY, ""); String text = messagePayload.optString(PAYLOAD_TEXT_KEY, ""); switch (purpose) { case PAYLOAD_PURPOSE_SHARE: onSharePayload(text); break; case PAYLOAD_PURPOSE_DEFINE: onDefinePayload(text); break; case PAYLOAD_PURPOSE_EDIT_HERE: onEditHerePayload(messagePayload.optInt("sectionID", 0), text); break; default: L.d("Unknown purpose=" + purpose); } } }); } public void showWiktionaryDefinition(String text) { PageTitle title = fragment.getTitle(); fragment.showBottomSheet(WiktionaryDialog.newInstance(title, text)); } private void onSharePayload(@NonNull String text) { if (funnel == null) { createFunnel(); } shareSnippet(text); funnel.logShareTap(text); } private void onDefinePayload(String text) { showWiktionaryDefinition(text.toLowerCase()); } private void onEditHerePayload(int sectionID, String text) { fragment.getEditHandler().startEditingSection(sectionID, text); } private void showCopySnackbar() { FeedbackUtil.showMessage(fragment.getActivity(), R.string.text_copied); } private void shareSnippet(@NonNull CharSequence input) { final String selectedText = StringUtil.sanitizeText(input.toString()); final PageTitle title = fragment.getTitle(); final String leadImageNameText = fragment.getPage().getPageProperties().getLeadImageName() != null ? fragment.getPage().getPageProperties().getLeadImageName() : ""; new ImageLicenseFetchClient().request(title.getWikiSite(), new PageTitle(Namespace.FILE.toLegacyString(), leadImageNameText, title.getWikiSite()), new ImageLicenseFetchClient.Callback() { @Override public void success(@NonNull Call<MwQueryResponse<MwQueryResponse.Pages>> call, @NonNull ImageLicense result) { final Bitmap snippetBitmap = SnippetImage.getSnippetImage(fragment.getContext(), fragment.getLeadImageBitmap(), title.getDisplayText(), fragment.getPage().isMainPage() ? "" : StringUtils.capitalize(title.getDescription()), selectedText, result); fragment.showBottomSheet(new PreviewDialog(fragment, snippetBitmap, title, selectedText, funnel)); } @Override public void failure(@NonNull Call<MwQueryResponse<MwQueryResponse.Pages>> call, @NonNull Throwable caught) { L.e("Error fetching image license info for " + title.getDisplayText(), caught); } }); } /** * @param mode ActionMode under which this context is starting. */ public void onTextSelected(CompatActionMode mode) { webViewActionMode = mode; Menu menu = mode.getMenu(); MenuItem shareItem = menu.findItem(R.id.menu_text_select_share); handleSelection(menu, shareItem); } private void handleSelection(Menu menu, MenuItem shareItem) { if (WikipediaApp.getInstance().getOnboardingStateMachine().isShareTutorialEnabled()) { postShowShareToolTip(shareItem); WikipediaApp.getInstance().getOnboardingStateMachine().setShareTutorial(); } // Provide our own listeners for the copy, define, and share buttons. shareItem.setOnMenuItemClickListener(new RequestTextSelectOnMenuItemClickListener(PAYLOAD_PURPOSE_SHARE)); MenuItem copyItem = menu.findItem(R.id.menu_text_select_copy); copyItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem menuItem) { fragment.getWebView().copyToClipboard(); showCopySnackbar(); leaveActionMode(); return true; } }); MenuItem defineItem = menu.findItem(R.id.menu_text_select_define); if (shouldEnableWiktionaryDialog()) { defineItem.setVisible(true); defineItem.setOnMenuItemClickListener(new RequestTextSelectOnMenuItemClickListener(PAYLOAD_PURPOSE_DEFINE)); } MenuItem editItem = menu.findItem(R.id.menu_text_edit_here); editItem.setOnMenuItemClickListener(new RequestTextSelectOnMenuItemClickListener(PAYLOAD_PURPOSE_EDIT_HERE)); if (!fragment.getPage().isArticle()) { editItem.setVisible(false); } createFunnel(); funnel.logHighlight(); } private boolean shouldEnableWiktionaryDialog() { return Prefs.useRestBase() && isWiktionaryDialogEnabledForArticleLanguage(); } private boolean isWiktionaryDialogEnabledForArticleLanguage() { return Arrays.asList(WiktionaryDialog.getEnabledLanguages()) .contains(fragment.getTitle().getWikiSite().languageCode()); } private void postShowShareToolTip(final MenuItem shareItem) { // There doesn't seem to be good lifecycle event accessible at the time this called to // ensure the tool tip is shown after CAB animation. final View shareItemView = ActivityUtil.getMenuItemView(fragment.getActivity(), shareItem); if (shareItemView != null) { int delay = getInteger(android.R.integer.config_longAnimTime); shareItemView.postDelayed(new Runnable() { @Override public void run() { showShareToolTip(shareItemView); } }, delay); } } private void showShareToolTip(View shareItemView) { ToolTipUtil.showToolTip(fragment.getActivity(), shareItemView, R.layout.inflate_tool_tip_share, getColor(SHARE_TOOL_TIP_COLOR), ToolTip.Position.CENTER); } @ColorInt private int getColor(@ColorRes int id) { return ContextCompat.getColor(fragment.getContext(), id); } private int getInteger(@IntegerRes int id) { return getResources().getInteger(id); } private Resources getResources() { return fragment.getContext().getResources(); } private void leaveActionMode() { if (hasWebViewActionMode()) { finishWebViewActionMode(); nullifyWebViewActionMode(); } } private boolean hasWebViewActionMode() { return webViewActionMode != null; } private void nullifyWebViewActionMode() { webViewActionMode = null; } private void finishWebViewActionMode() { webViewActionMode.finish(); } private class RequestTextSelectOnMenuItemClickListener implements MenuItem.OnMenuItemClickListener { @NonNull private final String purpose; RequestTextSelectOnMenuItemClickListener(@NonNull String purpose) { this.purpose = purpose; } @Override public boolean onMenuItemClick(MenuItem item) { requestTextSelection(purpose); leaveActionMode(); return true; } private void requestTextSelection(String purpose) { // send an event to the WebView that will make it return the // selected text (or first paragraph) back to us... try { JSONObject payload = new JSONObject(); payload.put(PAYLOAD_PURPOSE_KEY, purpose); bridge.sendMessage("getTextSelection", payload); } catch (JSONException e) { throw new RuntimeException(e); } } } } /** * A dialog to be displayed before sharing with two action buttons: * "Share as image", "Share as text". */ class PreviewDialog extends NoDimBottomSheetDialog { private boolean completed = false; PreviewDialog(final PageFragment fragment, final Bitmap resultBitmap, final PageTitle title, final String selectedText, final ShareAFactFunnel funnel) { super(fragment.getContext()); View rootView = LayoutInflater.from(fragment.getContext()).inflate(R.layout.dialog_share_preview, null); setContentView(rootView); ImageView previewImage = (ImageView) rootView.findViewById(R.id.preview_img); previewImage.setImageBitmap(resultBitmap); rootView.findViewById(R.id.close_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dismiss(); } }); rootView.findViewById(R.id.share_as_image_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String introText = fragment.getContext().getString(R.string.snippet_share_intro, title.getDisplayText(), UriUtil.getUrlWithProvenance(fragment.getContext(), title, R.string.prov_share_image)); ShareUtil.shareImage(fragment.getContext(), resultBitmap, title.getDisplayText(), title.getDisplayText(), introText); funnel.logShareIntent(selectedText, ShareMode.image); completed = true; } }); rootView.findViewById(R.id.share_as_text_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String introText = fragment.getContext().getString(R.string.snippet_share_intro, title.getDisplayText(), UriUtil.getUrlWithProvenance(fragment.getContext(), title, R.string.prov_share_text)); ShareUtil.shareText(fragment.getContext(), title.getDisplayText(), constructShareText(selectedText, introText)); funnel.logShareIntent(selectedText, ShareMode.text); completed = true; } }); setOnDismissListener(new OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { resultBitmap.recycle(); if (!completed) { funnel.logAbandoned(title.getDisplayText()); } } }); startExpanded(); } private String constructShareText(String selectedText, String introText) { return selectedText + "\n\n" + introText; } }