package org.wikipedia.edit.preview; import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.res.AssetManager; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ScrollView; import org.json.JSONException; import org.json.JSONObject; import org.wikipedia.R; import org.wikipedia.WikipediaApp; import org.wikipedia.analytics.EditFunnel; import org.wikipedia.bridge.CommunicationBridge; import org.wikipedia.bridge.DarkModeMarshaller; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.edit.EditSectionActivity; import org.wikipedia.edit.summaries.EditSummaryTag; import org.wikipedia.history.HistoryEntry; import org.wikipedia.page.LinkHandler; import org.wikipedia.page.PageActivity; import org.wikipedia.page.PageTitle; import org.wikipedia.util.ConfigurationCompat; import org.wikipedia.util.L10nUtil; import org.wikipedia.util.UriUtil; import org.wikipedia.util.log.L; import org.wikipedia.views.ObservableWebView; import org.wikipedia.views.ViewAnimations; import java.util.ArrayList; import java.util.List; import java.util.Locale; import retrofit2.Call; import static org.wikipedia.util.DeviceUtil.hideSoftKeyboard; public class EditPreviewFragment extends Fragment { private ObservableWebView webview; private ScrollView previewContainer; private EditSectionActivity parentActivity; private ViewGroup editSummaryTagsContainer; private String previewHTML; private CommunicationBridge bridge; private List<EditSummaryTag> summaryTags; private EditSummaryTag otherTag; private ProgressDialog progressDialog; private EditFunnel funnel; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View parent = inflater.inflate(R.layout.fragment_preview_edit, container, false); webview = (ObservableWebView) parent.findViewById(R.id.edit_preview_webview); previewContainer = (ScrollView) parent.findViewById(R.id.edit_preview_container); editSummaryTagsContainer = (ViewGroup) parent.findViewById(R.id.edit_summary_tags_container); bridge = new CommunicationBridge(webview, "file:///android_asset/preview.html"); return parent; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); parentActivity = (EditSectionActivity)getActivity(); PageTitle pageTitle = parentActivity.getPageTitle(); funnel = WikipediaApp.getInstance().getFunnelManager().getEditFunnel(pageTitle); /* Use a Resources object with a different Locale, so that the text of the canned summary buttons is shown in the selected Wiki language, instead of the current UI language. However, there's a caveat: creating a new Resources object actually modifies something internally in the AssetManager, so we'll need to create another new Resources object with the original Locale when we're done. https://code.google.com/p/android/issues/detail?id=67672 */ Resources oldResources = getResources(); AssetManager assets = oldResources.getAssets(); DisplayMetrics metrics = oldResources.getDisplayMetrics(); Locale oldLocale = ConfigurationCompat.getLocale(oldResources.getConfiguration()); Locale newLocale = new Locale(pageTitle.getWikiSite().languageCode()); Configuration config = new Configuration(oldResources.getConfiguration()); Resources tempResources = getResources(); if (!oldLocale.getLanguage().equals(newLocale.getLanguage())) { ConfigurationCompat.setLocale(config, newLocale); tempResources = new Resources(assets, metrics, config); } // build up summary tags... int[] summaryTagStrings = { R.string.edit_summary_tag_typo, R.string.edit_summary_tag_grammar, R.string.edit_summary_tag_links }; summaryTags = new ArrayList<>(); for (int i : summaryTagStrings) { final EditSummaryTag tag = new EditSummaryTag(getActivity()); tag.setText(tempResources.getString(i)); tag.setTag(i); tag.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { funnel.logEditSummaryTap((Integer) view.getTag()); tag.setSelected(!tag.getSelected()); } }); editSummaryTagsContainer.addView(tag); summaryTags.add(tag); } otherTag = new EditSummaryTag(getActivity()); otherTag.setText(tempResources.getString(R.string.edit_summary_tag_other)); editSummaryTagsContainer.addView(otherTag); otherTag.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { funnel.logEditSummaryTap(R.string.edit_summary_tag_other); if (otherTag.getSelected()) { otherTag.setSelected(false); } else { parentActivity.showCustomSummary(); } } }); /* Reset AssetManager to its original state, by creating a new Resources object with the original Locale (from above) */ if (!oldLocale.getLanguage().equals(newLocale.getLanguage())) { ConfigurationCompat.setLocale(config, oldLocale); new Resources(assets, metrics, config); } if (savedInstanceState != null) { for (int i = 0; i < summaryTags.size(); i++) { summaryTags.get(i).setSelected(savedInstanceState.getBoolean("summaryTag" + i, false)); } if (savedInstanceState.containsKey("otherTag")) { otherTag.setSelected(true); otherTag.setText(savedInstanceState.getString("otherTag")); } previewHTML = savedInstanceState.getString("previewHTML"); boolean isActive = savedInstanceState.getBoolean("isActive"); previewContainer.setVisibility(isActive ? View.VISIBLE : View.GONE); if (isActive) { displayPreview(previewHTML); } } progressDialog = new ProgressDialog(getActivity()); progressDialog.setIndeterminate(true); progressDialog.setMessage(getString(R.string.edit_preview_fetching_dialog_message)); progressDialog.setCancelable(false); } public void setCustomSummary(String summary) { otherTag.setText(summary.length() > 0 ? summary : getString(R.string.edit_summary_tag_other)); otherTag.setSelected(summary.length() > 0); } private boolean isWebViewSetup = false; private void displayPreview(final String html) { if (!isWebViewSetup) { isWebViewSetup = true; L10nUtil.setupDirectionality(parentActivity.getPageTitle().getWikiSite().languageCode(), Locale.getDefault().getLanguage(), bridge); if (WikipediaApp.getInstance().isCurrentThemeDark()) { new DarkModeMarshaller(bridge).turnOn(false); } bridge.addListener("linkClicked", new LinkHandler(getActivity()) { @Override public void onPageLinkClicked(String href) { // TODO: also need to handle references, issues, disambig, ... in preview eventually } @Override public void onUrlClick(@NonNull final String href, @Nullable final String titleString) { // Check if this is an internal link, and if it is then open it internally if (href.startsWith("/wiki/")) { PageTitle title = TextUtils.isEmpty(titleString) ? getWikiSite().titleForInternalLink(href) : PageTitle.withSeparateFragment(titleString, UriUtil.getFragment(href), getWikiSite()); onInternalLinkClicked(title); } else { //Show dialogue asking user to confirm they want to leave showLeavingEditDialogue(new Runnable() { @Override public void run() { openExternalLink(href, titleString); } }); } } @Override public void onInternalLinkClicked(final PageTitle title) { //Show dialogue asking user to confirm they want to leave showLeavingEditDialogue(new Runnable() { @Override public void run() { startActivity(PageActivity.newIntent(getContext(), new HistoryEntry(title, HistoryEntry.SOURCE_INTERNAL_LINK), title)); } }); } /** * Shows the user a dialogue asking them if they really meant to leave the edit * workflow, and warning them that their changes have not yet been saved. * @param runnable The runnable that is run if the user chooses to leave. */ private void showLeavingEditDialogue(final Runnable runnable) { //Ask the user if they really meant to leave the edit workflow final AlertDialog leavingEditDialog = new AlertDialog.Builder(getActivity()) .setMessage(R.string.dialog_message_leaving_edit) .setPositiveButton(R.string.dialog_message_leaving_edit_leave, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { //They meant to leave; close dialogue and run specified action dialog.dismiss(); runnable.run(); } }) .setNegativeButton(R.string.dialog_message_leaving_edit_stay, null) .create(); leavingEditDialog.show(); } /** * Open an external link. The method uses the onUrlClick method in the superclass of * of LinkHandler to do the heavy lifting. You can't call this method from inside a * Runnable or an AlertDialog, so we put it in here instead. * @param href The href of the external link to be opened. * @param titleString the title of the page to be openend as a string */ private void openExternalLink(String href, String titleString) { super.onUrlClick(href, titleString); } @Override public WikiSite getWikiSite() { return parentActivity.getPageTitle().getWikiSite(); } }); bridge.addListener("imageClicked", new CommunicationBridge.JSEventListener() { @Override public void onMessage(String messageType, JSONObject messagePayload) { // TODO: do something when an image is clicked in Preview. } }); bridge.addListener("mediaClicked", new CommunicationBridge.JSEventListener() { @Override public void onMessage(String messageType, JSONObject messagePayload) { // TODO: do something when a video is clicked in Preview. } }); bridge.addListener("referenceClicked", new CommunicationBridge.JSEventListener() { @Override public void onMessage(String messageType, JSONObject messagePayload) { // TODO: do something when a reference is clicked in Preview. } }); } ViewAnimations.fadeIn(previewContainer, new Runnable() { @Override public void run() { parentActivity.supportInvalidateOptionsMenu(); } }); ViewAnimations.fadeOut(getActivity().findViewById(R.id.edit_section_container)); JSONObject payload = new JSONObject(); try { payload.put("html", html); payload.put("siteBaseUrl", parentActivity.getPageTitle().getWikiSite().url()); } catch (JSONException e) { throw new RuntimeException(e); } bridge.sendMessage("displayPreviewHTML", payload); } /** * Fetches a preview of the modified text, and shows (fades in) the Preview fragment, * which includes edit summary tags. When the fade-in completes, the state of the * actionbar button(s) is updated, and the preview is shown. * @param title The PageTitle associated with the text being modified. * @param wikiText The text of the section to be shown in the Preview. */ public void showPreview(final PageTitle title, final String wikiText) { hideSoftKeyboard(getActivity()); progressDialog.show(); new EditPreviewClient().request(parentActivity.getPageTitle().getWikiSite(), title, wikiText, new EditPreviewClient.Callback() { @Override public void success(@NonNull Call<EditPreview> call, @NonNull String preview) { if (!progressDialog.isShowing()) { // no longer attached to activity! return; } displayPreview(preview); previewHTML = preview; parentActivity.supportInvalidateOptionsMenu(); progressDialog.dismiss(); } @Override public void failure(@NonNull Call<EditPreview> call, @NonNull Throwable caught) { if (!progressDialog.isShowing()) { // no longer attached to activity! return; } progressDialog.dismiss(); parentActivity.showError(caught); L.e(caught); } }); } /** * Gets the overall edit summary, as specified by the user by clicking various tags, * and/or entering a custom summary. * @return Summary of the edit. If the user clicked more than one summary tag, * they will be separated by commas. */ public String getSummary() { String summaryStr = ""; for (EditSummaryTag tag : summaryTags) { if (!tag.getSelected()) { continue; } if (summaryStr.length() > 0) { summaryStr += ", "; } summaryStr += tag; } if (otherTag.getSelected()) { if (summaryStr.length() > 0) { summaryStr += ", "; } summaryStr += otherTag; } return summaryStr; } @Override public void onDestroyView() { webview.destroy(); super.onDestroyView(); } public boolean handleBackPressed() { if (isActive()) { hide(); return true; } return false; } /** * Hides (fades out) the Preview fragment. * When fade-out completes, the state of the actionbar button(s) is updated. */ public void hide() { View editSectionContainer = getActivity().findViewById(R.id.edit_section_container); ViewAnimations.crossFade(previewContainer, editSectionContainer, new Runnable() { @Override public void run() { parentActivity.supportInvalidateOptionsMenu(); } }); } public boolean isActive() { return previewContainer.getVisibility() == View.VISIBLE; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString("previewHTML", previewHTML); outState.putBoolean("isActive", isActive()); for (int i = 0; i < summaryTags.size(); i++) { outState.putBoolean("summaryTag" + i, summaryTags.get(i).getSelected()); } if (otherTag.getSelected()) { outState.putString("otherTag", otherTag.toString()); } } @Override public void onDetach() { if (progressDialog.isShowing()) { progressDialog.dismiss(); } super.onDetach(); } }