diff options
Diffstat (limited to 'libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java')
-rwxr-xr-x | libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java | 1659 |
1 files changed, 1659 insertions, 0 deletions
diff --git a/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java new file mode 100755 index 000000000..9ff8df7d8 --- /dev/null +++ b/libs/editor/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java @@ -0,0 +1,1659 @@ +package org.wordpress.android.editor; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.res.Configuration; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Looper; +import android.os.SystemClock; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.text.Editable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.view.DragEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.webkit.URLUtil; +import android.webkit.WebView; +import android.widget.RelativeLayout.LayoutParams; +import android.widget.ToggleButton; + +import com.android.volley.toolbox.ImageLoader; + +import org.json.JSONException; +import org.json.JSONObject; +import org.wordpress.android.editor.EditorWebViewAbstract.ErrorListener; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; +import org.wordpress.android.util.DisplayUtils; +import org.wordpress.android.util.JSONUtils; +import org.wordpress.android.util.ProfilingUtils; +import org.wordpress.android.util.ShortcodeUtils; +import org.wordpress.android.util.StringUtils; +import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.UrlUtils; +import org.wordpress.android.util.helpers.MediaFile; +import org.wordpress.android.util.helpers.MediaGallery; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class EditorFragment extends EditorFragmentAbstract implements View.OnClickListener, View.OnTouchListener, + OnJsEditorStateChangedListener, OnImeBackListener, EditorWebViewAbstract.AuthHeaderRequestListener, + EditorMediaUploadListener { + private static final String ARG_PARAM_TITLE = "param_title"; + private static final String ARG_PARAM_CONTENT = "param_content"; + + private static final String JS_CALLBACK_HANDLER = "nativeCallbackHandler"; + + private static final String KEY_TITLE = "title"; + private static final String KEY_CONTENT = "content"; + + private static final String TAG_FORMAT_BAR_BUTTON_MEDIA = "media"; + private static final String TAG_FORMAT_BAR_BUTTON_LINK = "link"; + + private static final float TOOLBAR_ALPHA_ENABLED = 1; + private static final float TOOLBAR_ALPHA_DISABLED = 0.5f; + + private static final List<String> DRAGNDROP_SUPPORTED_MIMETYPES_TEXT = Arrays.asList(ClipDescription.MIMETYPE_TEXT_PLAIN, + ClipDescription.MIMETYPE_TEXT_HTML); + private static final List<String> DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE = Arrays.asList("image/jpeg", "image/png"); + + public static final int MAX_ACTION_TIME_MS = 2000; + + private String mTitle = ""; + private String mContentHtml = ""; + + private EditorWebViewAbstract mWebView; + private View mSourceView; + private SourceViewEditText mSourceViewTitle; + private SourceViewEditText mSourceViewContent; + + private int mSelectionStart; + private int mSelectionEnd; + + private String mFocusedFieldId; + + private String mTitlePlaceholder = ""; + private String mContentPlaceholder = ""; + + private boolean mDomHasLoaded = false; + private boolean mIsKeyboardOpen = false; + private boolean mEditorWasPaused = false; + private boolean mHideActionBarOnSoftKeyboardUp = false; + private boolean mIsFormatBarDisabled = false; + + private ConcurrentHashMap<String, MediaFile> mWaitingMediaFiles; + private Set<MediaGallery> mWaitingGalleries; + private Map<String, MediaType> mUploadingMedia; + private Set<String> mFailedMediaIds; + private MediaGallery mUploadingMediaGallery; + + private String mJavaScriptResult = ""; + + private CountDownLatch mGetTitleCountDownLatch; + private CountDownLatch mGetContentCountDownLatch; + private CountDownLatch mGetSelectedTextCountDownLatch; + + private final Map<String, ToggleButton> mTagToggleButtonMap = new HashMap<>(); + + private long mActionStartedAt = -1; + + private final View.OnDragListener mOnDragListener = new View.OnDragListener() { + private long lastSetCoordsTimestamp; + + private boolean isSupported(ClipDescription clipDescription, List<String> mimeTypesToCheck) { + if (clipDescription == null) { + return false; + } + + for (String supportedMimeType : mimeTypesToCheck) { + if (clipDescription.hasMimeType(supportedMimeType)) { + return true; + } + } + + return false; + } + + @Override + public boolean onDrag(View view, DragEvent dragEvent) { + switch (dragEvent.getAction()) { + case DragEvent.ACTION_DRAG_STARTED: + return isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_TEXT) || + isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE); + case DragEvent.ACTION_DRAG_ENTERED: + // would be nice to start marking the place the item will drop + break; + case DragEvent.ACTION_DRAG_LOCATION: + int x = DisplayUtils.pxToDp(getActivity(), (int) dragEvent.getX()); + int y = DisplayUtils.pxToDp(getActivity(), (int) dragEvent.getY()); + + // don't call into JS too often + long currentTimestamp = SystemClock.uptimeMillis(); + if ((currentTimestamp - lastSetCoordsTimestamp) > 150) { + lastSetCoordsTimestamp = currentTimestamp; + + mWebView.execJavaScriptFromString("ZSSEditor.moveCaretToCoords(" + x + ", " + y + ");"); + } + break; + case DragEvent.ACTION_DRAG_EXITED: + // clear any drop marking maybe + break; + case DragEvent.ACTION_DROP: + if (mSourceView.getVisibility() == View.VISIBLE) { + if (isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE)) { + // don't allow dropping images into the HTML source + ToastUtils.showToast(getActivity(), R.string.editor_dropped_html_images_not_allowed, + ToastUtils.Duration.LONG); + return true; + } else { + // let the system handle the text drop + return false; + } + } + + if (isSupported(dragEvent.getClipDescription(), DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE) && + ("zss_field_title".equals(mFocusedFieldId))) { + // don't allow dropping images into the title field + ToastUtils.showToast(getActivity(), R.string.editor_dropped_title_images_not_allowed, + ToastUtils.Duration.LONG); + return true; + } + + if (isAdded()) { + mEditorDragAndDropListener.onRequestDragAndDropPermissions(dragEvent); + } + + ClipDescription clipDescription = dragEvent.getClipDescription(); + if (clipDescription.getMimeTypeCount() < 1) { + break; + } + + ContentResolver contentResolver = getActivity().getContentResolver(); + ArrayList<Uri> uris = new ArrayList<>(); + boolean unsupportedDropsFound = false; + + for (int i = 0; i < dragEvent.getClipData().getItemCount(); i++) { + ClipData.Item item = dragEvent.getClipData().getItemAt(i); + Uri uri = item.getUri(); + + final String uriType = uri != null ? contentResolver.getType(uri) : null; + if (uriType != null && DRAGNDROP_SUPPORTED_MIMETYPES_IMAGE.contains(uriType)) { + uris.add(uri); + continue; + } else if (item.getText() != null) { + insertTextToEditor(item.getText().toString()); + continue; + } else if (item.getHtmlText() != null) { + insertTextToEditor(item.getHtmlText()); + continue; + } + + // any other drop types are not supported, including web URLs. We cannot proactively + // determine their mime type for filtering + unsupportedDropsFound = true; + } + + if (unsupportedDropsFound) { + ToastUtils.showToast(getActivity(), R.string.editor_dropped_unsupported_files, ToastUtils + .Duration.LONG); + } + + if (uris.size() > 0) { + mEditorDragAndDropListener.onMediaDropped(uris); + } + + break; + case DragEvent.ACTION_DRAG_ENDED: + // clear any drop marking maybe + default: + break; + } + return true; + } + + private void insertTextToEditor(String text) { + if (text != null) { + mWebView.execJavaScriptFromString("ZSSEditor.insertText('" + Utils.escapeHtml(text) + "', true);"); + } else { + ToastUtils.showToast(getActivity(), R.string.editor_dropped_text_error, ToastUtils.Duration.SHORT); + AppLog.d(T.EDITOR, "Dropped text was null!"); + } + } + }; + + public static EditorFragment newInstance(String title, String content) { + EditorFragment fragment = new EditorFragment(); + Bundle args = new Bundle(); + args.putString(ARG_PARAM_TITLE, title); + args.putString(ARG_PARAM_CONTENT, content); + fragment.setArguments(args); + return fragment; + } + + public EditorFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ProfilingUtils.start("Visual Editor Startup"); + ProfilingUtils.split("EditorFragment.onCreate"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_editor, container, false); + + // Setup hiding the action bar when the soft keyboard is displayed for narrow viewports + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE + && !getResources().getBoolean(R.bool.is_large_tablet_landscape)) { + mHideActionBarOnSoftKeyboardUp = true; + } + + mWaitingMediaFiles = new ConcurrentHashMap<>(); + mWaitingGalleries = Collections.newSetFromMap(new ConcurrentHashMap<MediaGallery, Boolean>()); + mUploadingMedia = new HashMap<>(); + mFailedMediaIds = new HashSet<>(); + + // -- WebView configuration + + mWebView = (EditorWebViewAbstract) view.findViewById(R.id.webview); + + // Revert to compatibility WebView for custom ROMs using a 4.3 WebView in Android 4.4 + if (mWebView.shouldSwitchToCompatibilityMode()) { + ViewGroup parent = (ViewGroup) mWebView.getParent(); + int index = parent.indexOfChild(mWebView); + parent.removeView(mWebView); + mWebView = new EditorWebViewCompatibility(getActivity(), null); + mWebView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + parent.addView(mWebView, index); + } + + mWebView.setOnTouchListener(this); + mWebView.setOnImeBackListener(this); + mWebView.setAuthHeaderRequestListener(this); + + mWebView.setOnDragListener(mOnDragListener); + + if (mCustomHttpHeaders != null && mCustomHttpHeaders.size() > 0) { + for (Map.Entry<String, String> entry : mCustomHttpHeaders.entrySet()) { + mWebView.setCustomHeader(entry.getKey(), entry.getValue()); + } + } + + // Ensure that the content field is always filling the remaining screen space + mWebView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + mWebView.post(new Runnable() { + @Override + public void run() { + mWebView.execJavaScriptFromString("try {ZSSEditor.refreshVisibleViewportSize();} catch (e) " + + "{console.log(e)}"); + } + }); + } + }); + + mEditorFragmentListener.onEditorFragmentInitialized(); + + initJsEditor(); + + if (savedInstanceState != null) { + setTitle(savedInstanceState.getCharSequence(KEY_TITLE)); + setContent(savedInstanceState.getCharSequence(KEY_CONTENT)); + } + + // -- HTML mode configuration + + mSourceView = view.findViewById(R.id.sourceview); + mSourceViewTitle = (SourceViewEditText) view.findViewById(R.id.sourceview_title); + mSourceViewContent = (SourceViewEditText) view.findViewById(R.id.sourceview_content); + + // Toggle format bar on/off as user changes focus between title and content in HTML mode + mSourceViewTitle.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + updateFormatBarEnabledState(!hasFocus); + } + }); + + mSourceViewTitle.setOnTouchListener(this); + mSourceViewContent.setOnTouchListener(this); + + mSourceViewTitle.setOnImeBackListener(this); + mSourceViewContent.setOnImeBackListener(this); + + mSourceViewContent.addTextChangedListener(new HtmlStyleTextWatcher()); + + mSourceViewTitle.setHint(mTitlePlaceholder); + mSourceViewContent.setHint("<p>" + mContentPlaceholder + "</p>"); + + // attach drag-and-drop handler + mSourceViewTitle.setOnDragListener(mOnDragListener); + mSourceViewContent.setOnDragListener(mOnDragListener); + + // -- Format bar configuration + + setupFormatBarButtonMap(view); + + return view; + } + + @Override + public void onPause() { + super.onPause(); + mEditorWasPaused = true; + mIsKeyboardOpen = false; + } + + @Override + public void onResume() { + super.onResume(); + // If the editor was previously paused and the current orientation is landscape, + // hide the actionbar because the keyboard is going to appear (even if it was hidden + // prior to being paused). + if (mEditorWasPaused + && (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) + && !getResources().getBoolean(R.bool.is_large_tablet_landscape)) { + mIsKeyboardOpen = true; + mHideActionBarOnSoftKeyboardUp = true; + hideActionBarIfNeeded(); + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mEditorDragAndDropListener = (EditorDragAndDropListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + " must implement EditorDragAndDropListener"); + } + } + + @Override + public void onDetach() { + // Soft cancel (delete flag off) all media uploads currently in progress + for (String mediaId : mUploadingMedia.keySet()) { + mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, false); + } + super.onDetach(); + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + if (mDomHasLoaded) { + mWebView.notifyVisibilityChanged(isVisibleToUser); + } + super.setUserVisibleHint(isVisibleToUser); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putCharSequence(KEY_TITLE, getTitle()); + outState.putCharSequence(KEY_CONTENT, getContent()); + } + + private ActionBar getActionBar() { + if (!isAdded()) { + return null; + } + + if (getActivity() instanceof AppCompatActivity) { + return ((AppCompatActivity) getActivity()).getSupportActionBar(); + } else { + return null; + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + if (getView() != null) { + // Reload the format bar to make sure the correct one for the new screen width is being used + View formatBar = getView().findViewById(R.id.format_bar); + + if (formatBar != null) { + // Remember the currently active format bar buttons so they can be re-activated after the reload + ArrayList<String> activeTags = new ArrayList<>(); + for (Map.Entry<String, ToggleButton> entry : mTagToggleButtonMap.entrySet()) { + if (entry.getValue().isChecked()) { + activeTags.add(entry.getKey()); + } + } + + ViewGroup parent = (ViewGroup) formatBar.getParent(); + parent.removeView(formatBar); + + formatBar = getActivity().getLayoutInflater().inflate(R.layout.format_bar, parent, false); + formatBar.setId(R.id.format_bar); + parent.addView(formatBar); + + setupFormatBarButtonMap(formatBar); + + if (mIsFormatBarDisabled) { + updateFormatBarEnabledState(false); + } + + // Restore the active format bar buttons + for (String tag : activeTags) { + mTagToggleButtonMap.get(tag).setChecked(true); + } + + if (mSourceView.getVisibility() == View.VISIBLE) { + ToggleButton htmlButton = (ToggleButton) formatBar.findViewById(R.id.format_bar_button_html); + htmlButton.setChecked(true); + } + } + + // Reload HTML mode margins + View sourceViewTitle = getView().findViewById(R.id.sourceview_title); + View sourceViewContent = getView().findViewById(R.id.sourceview_content); + + if (sourceViewTitle != null && sourceViewContent != null) { + int sideMargin = (int) getActivity().getResources().getDimension(R.dimen.sourceview_side_margin); + + ViewGroup.MarginLayoutParams titleParams = + (ViewGroup.MarginLayoutParams) sourceViewTitle.getLayoutParams(); + ViewGroup.MarginLayoutParams contentParams = + (ViewGroup.MarginLayoutParams) sourceViewContent.getLayoutParams(); + + titleParams.setMargins(sideMargin, titleParams.topMargin, sideMargin, titleParams.bottomMargin); + contentParams.setMargins(sideMargin, contentParams.topMargin, sideMargin, contentParams.bottomMargin); + } + } + + // Toggle action bar auto-hiding for the new orientation + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE + && !getResources().getBoolean(R.bool.is_large_tablet_landscape)) { + mHideActionBarOnSoftKeyboardUp = true; + hideActionBarIfNeeded(); + } else { + mHideActionBarOnSoftKeyboardUp = false; + showActionBarIfNeeded(); + } + } + + private void setupFormatBarButtonMap(View view) { + ToggleButton boldButton = (ToggleButton) view.findViewById(R.id.format_bar_button_bold); + mTagToggleButtonMap.put(getString(R.string.format_bar_tag_bold), boldButton); + + ToggleButton italicButton = (ToggleButton) view.findViewById(R.id.format_bar_button_italic); + mTagToggleButtonMap.put(getString(R.string.format_bar_tag_italic), italicButton); + + ToggleButton quoteButton = (ToggleButton) view.findViewById(R.id.format_bar_button_quote); + mTagToggleButtonMap.put(getString(R.string.format_bar_tag_blockquote), quoteButton); + + ToggleButton ulButton = (ToggleButton) view.findViewById(R.id.format_bar_button_ul); + mTagToggleButtonMap.put(getString(R.string.format_bar_tag_unorderedList), ulButton); + + ToggleButton olButton = (ToggleButton) view.findViewById(R.id.format_bar_button_ol); + mTagToggleButtonMap.put(getString(R.string.format_bar_tag_orderedList), olButton); + + // Tablet-only + ToggleButton strikethroughButton = (ToggleButton) view.findViewById(R.id.format_bar_button_strikethrough); + if (strikethroughButton != null) { + mTagToggleButtonMap.put(getString(R.string.format_bar_tag_strikethrough), strikethroughButton); + } + + ToggleButton mediaButton = (ToggleButton) view.findViewById(R.id.format_bar_button_media); + mTagToggleButtonMap.put(TAG_FORMAT_BAR_BUTTON_MEDIA, mediaButton); + + registerForContextMenu(mediaButton); + + ToggleButton linkButton = (ToggleButton) view.findViewById(R.id.format_bar_button_link); + mTagToggleButtonMap.put(TAG_FORMAT_BAR_BUTTON_LINK, linkButton); + + ToggleButton htmlButton = (ToggleButton) view.findViewById(R.id.format_bar_button_html); + htmlButton.setOnClickListener(this); + + for (ToggleButton button : mTagToggleButtonMap.values()) { + button.setOnClickListener(this); + } + } + + protected void initJsEditor() { + if (!isAdded()) { + return; + } + + ProfilingUtils.split("EditorFragment.initJsEditor"); + + String htmlEditor = Utils.getHtmlFromFile(getActivity(), "android-editor.html"); + if (htmlEditor != null) { + htmlEditor = htmlEditor.replace("%%TITLE%%", getString(R.string.visual_editor)); + htmlEditor = htmlEditor.replace("%%ANDROID_API_LEVEL%%", String.valueOf(Build.VERSION.SDK_INT)); + htmlEditor = htmlEditor.replace("%%LOCALIZED_STRING_INIT%%", + "nativeState.localizedStringEdit = '" + getString(R.string.edit) + "';\n" + + "nativeState.localizedStringUploading = '" + getString(R.string.uploading) + "';\n" + + "nativeState.localizedStringUploadingGallery = '" + getString(R.string.uploading_gallery_placeholder) + "';\n"); + } + + // To avoid reflection security issues with JavascriptInterface on API<17, we use an iframe to make URL requests + // for callbacks from JS instead. These are received by WebViewClient.shouldOverrideUrlLoading() and then + // passed on to the JsCallbackReceiver + if (Build.VERSION.SDK_INT < 17) { + mWebView.setJsCallbackReceiver(new JsCallbackReceiver(this)); + } else { + mWebView.addJavascriptInterface(new JsCallbackReceiver(this), JS_CALLBACK_HANDLER); + } + + mWebView.loadDataWithBaseURL("file:///android_asset/", htmlEditor, "text/html", "utf-8", ""); + + if (mDebugModeEnabled) { + enableWebDebugging(true); + } + } + + public void checkForFailedUploadAndSwitchToHtmlMode(final ToggleButton toggleButton) { + if (!isAdded()) { + return; + } + + // Show an Alert Dialog asking the user if he wants to remove all failed media before upload + if (hasFailedMediaUploads()) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage(R.string.editor_failed_uploads_switch_html) + .setPositiveButton(R.string.editor_remove_failed_uploads, new OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + // Clear failed uploads and switch to HTML mode + removeAllFailedMediaUploads(); + toggleHtmlMode(toggleButton); + } + }).setNegativeButton(android.R.string.cancel, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + toggleButton.setChecked(false); + } + }); + builder.create().show(); + } else { + toggleHtmlMode(toggleButton); + } + } + + public boolean isActionInProgress() { + return System.currentTimeMillis() - mActionStartedAt < MAX_ACTION_TIME_MS; + } + + private void toggleHtmlMode(final ToggleButton toggleButton) { + if (!isAdded()) { + return; + } + + mEditorFragmentListener.onTrackableEvent(TrackableEvent.HTML_BUTTON_TAPPED); + + // Don't switch to HTML mode if currently uploading media + if (!mUploadingMedia.isEmpty() || isActionInProgress()) { + toggleButton.setChecked(false); + ToastUtils.showToast(getActivity(), R.string.alert_action_while_uploading, ToastUtils.Duration.LONG); + return; + } + + clearFormatBarButtons(); + updateFormatBarEnabledState(true); + + if (toggleButton.isChecked()) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + if (!isAdded()) { + return; + } + + // Update mTitle and mContentHtml with the latest state from the ZSSEditor + getTitle(); + getContent(); + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + // Set HTML mode state + mSourceViewTitle.setText(mTitle); + + SpannableString spannableContent = new SpannableString(mContentHtml); + HtmlStyleUtils.styleHtmlForDisplay(spannableContent); + mSourceViewContent.setText(spannableContent); + + mWebView.setVisibility(View.GONE); + mSourceView.setVisibility(View.VISIBLE); + + mSourceViewContent.requestFocus(); + mSourceViewContent.setSelection(0); + + InputMethodManager imm = ((InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE)); + imm.showSoftInput(mSourceViewContent, InputMethodManager.SHOW_IMPLICIT); + } + }); + } + }); + + thread.start(); + + } else { + mWebView.setVisibility(View.VISIBLE); + mSourceView.setVisibility(View.GONE); + + mTitle = mSourceViewTitle.getText().toString(); + mContentHtml = mSourceViewContent.getText().toString(); + updateVisualEditorFields(); + + // Update the list of failed media uploads + mWebView.execJavaScriptFromString("ZSSEditor.getFailedMedia();"); + + // Reset selection to avoid buggy cursor behavior + mWebView.execJavaScriptFromString("ZSSEditor.resetSelectionOnField('zss_field_content');"); + } + } + + private void displayLinkDialog() { + final LinkDialogFragment linkDialogFragment = new LinkDialogFragment(); + linkDialogFragment.setTargetFragment(this, LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_ADD); + + final Bundle dialogBundle = new Bundle(); + + // Pass potential URL from user clipboard + String clipboardUri = Utils.getUrlFromClipboard(getActivity()); + if (clipboardUri != null) { + dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_URL, clipboardUri); + } + + // Pass selected text to dialog + if (mSourceView.getVisibility() == View.VISIBLE) { + // HTML mode + mSelectionStart = mSourceViewContent.getSelectionStart(); + mSelectionEnd = mSourceViewContent.getSelectionEnd(); + + String selectedText = mSourceViewContent.getText().toString().substring(mSelectionStart, mSelectionEnd); + dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT, selectedText); + + linkDialogFragment.setArguments(dialogBundle); + linkDialogFragment.show(getFragmentManager(), LinkDialogFragment.class.getSimpleName()); + } else { + // Visual mode + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + if (!isAdded()) { + return; + } + + mGetSelectedTextCountDownLatch = new CountDownLatch(1); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mWebView.execJavaScriptFromString( + "ZSSEditor.execFunctionForResult('getSelectedTextToLinkify');"); + } + }); + + try { + if (mGetSelectedTextCountDownLatch.await(1, TimeUnit.SECONDS)) { + dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT, mJavaScriptResult); + } + } catch (InterruptedException e) { + AppLog.d(T.EDITOR, "Failed to obtain selected text from JS editor."); + } + + linkDialogFragment.setArguments(dialogBundle); + linkDialogFragment.show(getFragmentManager(), LinkDialogFragment.class.getSimpleName()); + } + }); + + thread.start(); + } + } + + @Override + public void onClick(View v) { + if (!isAdded()) { + return; + } + + int id = v.getId(); + if (id == R.id.format_bar_button_html) { + checkForFailedUploadAndSwitchToHtmlMode((ToggleButton) v); + } else if (id == R.id.format_bar_button_media) { + mEditorFragmentListener.onTrackableEvent(TrackableEvent.MEDIA_BUTTON_TAPPED); + ((ToggleButton) v).setChecked(false); + + if (isActionInProgress()) { + ToastUtils.showToast(getActivity(), R.string.alert_action_while_uploading, ToastUtils.Duration.LONG); + return; + } + + if (mSourceView.getVisibility() == View.VISIBLE) { + ToastUtils.showToast(getActivity(), R.string.alert_insert_image_html_mode, ToastUtils.Duration.LONG); + } else { + mEditorFragmentListener.onAddMediaClicked(); + getActivity().openContextMenu(mTagToggleButtonMap.get(TAG_FORMAT_BAR_BUTTON_MEDIA)); + } + } else if (id == R.id.format_bar_button_link) { + if (!((ToggleButton) v).isChecked()) { + // The link button was checked when it was pressed; remove the current link + mWebView.execJavaScriptFromString("ZSSEditor.unlink();"); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.UNLINK_BUTTON_TAPPED); + return; + } + mEditorFragmentListener.onTrackableEvent(TrackableEvent.LINK_BUTTON_TAPPED); + + ((ToggleButton) v).setChecked(false); + + displayLinkDialog(); + } else { + if (v instanceof ToggleButton) { + onFormattingButtonClicked((ToggleButton) v); + } + } + } + + @Override + public boolean onTouch(View view, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + // If the WebView or EditText has received a touch event, the keyboard will be displayed and the action bar + // should hide + mIsKeyboardOpen = true; + hideActionBarIfNeeded(); + } + return false; + } + + /** + * Intercept back button press while soft keyboard is visible. + */ + @Override + public void onImeBack() { + mIsKeyboardOpen = false; + showActionBarIfNeeded(); + } + + @Override + public String onAuthHeaderRequested(String url) { + return mEditorFragmentListener.onAuthHeaderRequested(url); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if ((requestCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_ADD || + requestCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_UPDATE)) { + + if (resultCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_DELETE) { + mWebView.execJavaScriptFromString("ZSSEditor.unlink();"); + return; + } + + if (data == null) { + return; + } + + Bundle extras = data.getExtras(); + if (extras == null) { + return; + } + + String linkUrl = extras.getString(LinkDialogFragment.LINK_DIALOG_ARG_URL); + String linkText = extras.getString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT); + + if (linkText == null || linkText.equals("")) { + linkText = linkUrl; + } + + if (TextUtils.isEmpty(Uri.parse(linkUrl).getScheme())) linkUrl = "http://" + linkUrl; + + if (mSourceView.getVisibility() == View.VISIBLE) { + Editable content = mSourceViewContent.getText(); + if (content == null) { + return; + } + + if (mSelectionStart < mSelectionEnd) { + content.delete(mSelectionStart, mSelectionEnd); + } + + String urlHtml = "<a href=\"" + linkUrl + "\">" + linkText + "</a>"; + + content.insert(mSelectionStart, urlHtml); + mSourceViewContent.setSelection(mSelectionStart + urlHtml.length()); + } else { + String jsMethod; + if (requestCode == LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_ADD) { + jsMethod = "ZSSEditor.insertLink"; + } else { + jsMethod = "ZSSEditor.updateLink"; + } + mWebView.execJavaScriptFromString(jsMethod + "('" + Utils.escapeHtml(linkUrl) + "', '" + + Utils.escapeHtml(linkText) + "');"); + } + } else if (requestCode == ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_REQUEST_CODE) { + if (data == null) { + mWebView.execJavaScriptFromString("ZSSEditor.clearCurrentEditingImage();"); + return; + } + + Bundle extras = data.getExtras(); + if (extras == null) { + return; + } + + final String imageMeta = Utils.escapeQuotes(StringUtils.notNullStr(extras.getString("imageMeta"))); + final int imageRemoteId = extras.getInt("imageRemoteId"); + final boolean isFeaturedImage = extras.getBoolean("isFeatured"); + + mWebView.post(new Runnable() { + @Override + public void run() { + mWebView.execJavaScriptFromString("ZSSEditor.updateCurrentImageMeta('" + imageMeta + "');"); + } + }); + + if (imageRemoteId != 0) { + if (isFeaturedImage) { + mFeaturedImageId = imageRemoteId; + mEditorFragmentListener.onFeaturedImageChanged(mFeaturedImageId); + } else { + // If this image was unset as featured, clear the featured image id + if (mFeaturedImageId == imageRemoteId) { + mFeaturedImageId = 0; + mEditorFragmentListener.onFeaturedImageChanged(mFeaturedImageId); + } + } + } + } + } + + @SuppressLint("NewApi") + private void enableWebDebugging(boolean enable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + AppLog.i(T.EDITOR, "Enabling web debugging"); + WebView.setWebContentsDebuggingEnabled(enable); + } + mWebView.setDebugModeEnabled(mDebugModeEnabled); + } + + @Override + public void setTitle(CharSequence text) { + mTitle = text.toString(); + } + + @Override + public void setContent(CharSequence text) { + mContentHtml = text.toString(); + } + + /** + * Returns the contents of the title field from the JavaScript editor. Should be called from a background thread + * where possible. + */ + @Override + public CharSequence getTitle() { + if (!isAdded()) { + return ""; + } + + if (mSourceView != null && mSourceView.getVisibility() == View.VISIBLE) { + mTitle = mSourceViewTitle.getText().toString(); + return StringUtils.notNullStr(mTitle); + } + + if (Looper.myLooper() == Looper.getMainLooper()) { + AppLog.d(T.EDITOR, "getTitle() called from UI thread"); + } + + mGetTitleCountDownLatch = new CountDownLatch(1); + + // All WebView methods must be called from the UI thread + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_title').getHTMLForCallback();"); + } + }); + + try { + mGetTitleCountDownLatch.await(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + AppLog.e(T.EDITOR, e); + Thread.currentThread().interrupt(); + } + + return StringUtils.notNullStr(mTitle.replaceAll(" $", "")); + } + + /** + * Returns the contents of the content field from the JavaScript editor. Should be called from a background thread + * where possible. + */ + @Override + public CharSequence getContent() { + if (!isAdded()) { + return ""; + } + + if (mSourceView != null && mSourceView.getVisibility() == View.VISIBLE) { + mContentHtml = mSourceViewContent.getText().toString(); + return StringUtils.notNullStr(mContentHtml); + } + + if (Looper.myLooper() == Looper.getMainLooper()) { + AppLog.d(T.EDITOR, "getContent() called from UI thread"); + } + + mGetContentCountDownLatch = new CountDownLatch(1); + + // All WebView methods must be called from the UI thread + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').getHTMLForCallback();"); + } + }); + + try { + mGetContentCountDownLatch.await(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + AppLog.e(T.EDITOR, e); + Thread.currentThread().interrupt(); + } + + return StringUtils.notNullStr(mContentHtml); + } + + @Override + public void appendMediaFile(final MediaFile mediaFile, final String mediaUrl, ImageLoader imageLoader) { + if (!mDomHasLoaded) { + // If the DOM hasn't loaded yet, we won't be able to add media to the ZSSEditor + // Place them in a queue to be handled when the DOM loaded callback is received + mWaitingMediaFiles.put(mediaUrl, mediaFile); + return; + } + + final String safeMediaUrl = Utils.escapeQuotes(mediaUrl); + + mWebView.post(new Runnable() { + @Override + public void run() { + if (URLUtil.isNetworkUrl(mediaUrl)) { + String mediaId = mediaFile.getMediaId(); + if (mediaFile.isVideo()) { + String posterUrl = Utils.escapeQuotes(StringUtils.notNullStr(mediaFile.getThumbnailURL())); + String videoPressId = ShortcodeUtils.getVideoPressIdFromShortCode( + mediaFile.getVideoPressShortCode()); + + mWebView.execJavaScriptFromString("ZSSEditor.insertVideo('" + safeMediaUrl + "', '" + + posterUrl + "', '" + videoPressId + "');"); + } else { + mWebView.execJavaScriptFromString("ZSSEditor.insertImage('" + safeMediaUrl + "', '" + mediaId + + "');"); + } + mActionStartedAt = System.currentTimeMillis(); + } else { + String id = mediaFile.getMediaId(); + if (mediaFile.isVideo()) { + String posterUrl = Utils.escapeQuotes(StringUtils.notNullStr(mediaFile.getThumbnailURL())); + mWebView.execJavaScriptFromString("ZSSEditor.insertLocalVideo(" + id + ", '" + posterUrl + + "');"); + mUploadingMedia.put(id, MediaType.VIDEO); + } else { + mWebView.execJavaScriptFromString("ZSSEditor.insertLocalImage(" + id + ", '" + safeMediaUrl + + "');"); + mUploadingMedia.put(id, MediaType.IMAGE); + } + } + } + }); + } + + @Override + public void appendGallery(MediaGallery mediaGallery) { + if (!mDomHasLoaded) { + // If the DOM hasn't loaded yet, we won't be able to add a gallery to the ZSSEditor + // Place it in a queue to be handled when the DOM loaded callback is received + mWaitingGalleries.add(mediaGallery); + return; + } + + if (mediaGallery.getIds().isEmpty()) { + mUploadingMediaGallery = mediaGallery; + mWebView.execJavaScriptFromString("ZSSEditor.insertLocalGallery('" + mediaGallery.getUniqueId() + "');"); + } else { + // Ensure that the content field is in focus (it may not be if we're adding a gallery to a new post by a + // share action and not via the format bar button) + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();"); + + mWebView.execJavaScriptFromString("ZSSEditor.insertGallery('" + mediaGallery.getIdsStr() + "', '" + + mediaGallery.getType() + "', " + mediaGallery.getNumColumns() + ");"); + } + } + + @Override + public void setUrlForVideoPressId(final String videoId, final String videoUrl, final String posterUrl) { + mWebView.post(new Runnable() { + @Override + public void run() { + mWebView.execJavaScriptFromString("ZSSEditor.setVideoPressLinks('" + videoId + "', '" + + Utils.escapeQuotes(videoUrl) + "', '" + Utils.escapeQuotes(posterUrl) + "');"); + } + }); + } + + @Override + public boolean isUploadingMedia() { + return (mUploadingMedia.size() > 0); + } + + @Override + public boolean hasFailedMediaUploads() { + return (mFailedMediaIds.size() > 0); + } + + @Override + public void removeAllFailedMediaUploads() { + mWebView.execJavaScriptFromString("ZSSEditor.removeAllFailedMediaUploads();"); + } + + @Override + public Spanned getSpannedContent() { + return null; + } + + @Override + public void setTitlePlaceholder(CharSequence placeholderText) { + mTitlePlaceholder = placeholderText.toString(); + } + + @Override + public void setContentPlaceholder(CharSequence placeholderText) { + mContentPlaceholder = placeholderText.toString(); + } + + @Override + public void onMediaUploadSucceeded(final String localMediaId, final MediaFile mediaFile) { + final MediaType mediaType = mUploadingMedia.get(localMediaId); + if (mediaType != null) { + mWebView.post(new Runnable() { + @Override + public void run() { + String remoteUrl = Utils.escapeQuotes(mediaFile.getFileURL()); + if (mediaType.equals(MediaType.IMAGE)) { + String remoteMediaId = mediaFile.getMediaId(); + mWebView.execJavaScriptFromString("ZSSEditor.replaceLocalImageWithRemoteImage(" + localMediaId + + ", '" + remoteMediaId + "', '" + remoteUrl + "');"); + } else if (mediaType.equals(MediaType.VIDEO)) { + String posterUrl = Utils.escapeQuotes(StringUtils.notNullStr(mediaFile.getThumbnailURL())); + String videoPressId = ShortcodeUtils.getVideoPressIdFromShortCode( + mediaFile.getVideoPressShortCode()); + mWebView.execJavaScriptFromString("ZSSEditor.replaceLocalVideoWithRemoteVideo(" + localMediaId + + ", '" + remoteUrl + "', '" + posterUrl + "', '" + videoPressId + "');"); + } + } + }); + } + } + + @Override + public void onMediaUploadProgress(final String mediaId, final float progress) { + final MediaType mediaType = mUploadingMedia.get(mediaId); + if (mediaType != null) { + mWebView.post(new Runnable() { + @Override + public void run() { + String progressString = String.format(Locale.US, "%.1f", progress); + mWebView.execJavaScriptFromString("ZSSEditor.setProgressOnMedia(" + mediaId + ", " + + progressString + ");"); + } + }); + } + } + + @Override + public void onMediaUploadFailed(final String mediaId, final String errorMessage) { + mWebView.post(new Runnable() { + @Override + public void run() { + MediaType mediaType = mUploadingMedia.get(mediaId); + if (mediaType != null) { + switch (mediaType) { + case IMAGE: + mWebView.execJavaScriptFromString("ZSSEditor.markImageUploadFailed(" + mediaId + ", '" + + Utils.escapeQuotes(errorMessage) + "');"); + break; + case VIDEO: + mWebView.execJavaScriptFromString("ZSSEditor.markVideoUploadFailed(" + mediaId + ", '" + + Utils.escapeQuotes(errorMessage) + "');"); + } + mFailedMediaIds.add(mediaId); + mUploadingMedia.remove(mediaId); + } + } + }); + } + + @Override + public void onGalleryMediaUploadSucceeded(final long galleryId, String remoteMediaId, int remaining) { + if (galleryId == mUploadingMediaGallery.getUniqueId()) { + ArrayList<String> mediaIds = mUploadingMediaGallery.getIds(); + mediaIds.add(remoteMediaId); + mUploadingMediaGallery.setIds(mediaIds); + + if (remaining == 0) { + mWebView.post(new Runnable() { + @Override + public void run() { + mWebView.execJavaScriptFromString("ZSSEditor.replacePlaceholderGallery('" + galleryId + "', '" + + mUploadingMediaGallery.getIdsStr() + "', '" + + mUploadingMediaGallery.getType() + "', " + + mUploadingMediaGallery.getNumColumns() + ");"); + } + }); + } + } + } + + public void onDomLoaded() { + ProfilingUtils.split("EditorFragment.onDomLoaded"); + + mWebView.post(new Runnable() { + public void run() { + if (!isAdded()) { + return; + } + + mDomHasLoaded = true; + + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setMultiline('true');"); + + // Set title and content placeholder text + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_title').setPlaceholderText('" + + Utils.escapeQuotes(mTitlePlaceholder) + "');"); + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setPlaceholderText('" + + Utils.escapeQuotes(mContentPlaceholder) + "');"); + + // Load title and content into ZSSEditor + updateVisualEditorFields(); + + // If there are images that are still in progress (because the editor exited before they completed), + // set them to failed, so the user can restart them (otherwise they will stay stuck in 'uploading' mode) + mWebView.execJavaScriptFromString("ZSSEditor.markAllUploadingMediaAsFailed('" + + Utils.escapeQuotes(getString(R.string.tap_to_try_again)) + "');"); + + // Update the list of failed media uploads + mWebView.execJavaScriptFromString("ZSSEditor.getFailedMedia();"); + + hideActionBarIfNeeded(); + + // Reset all format bar buttons (in case they remained active through activity re-creation) + ToggleButton htmlButton = (ToggleButton) getActivity().findViewById(R.id.format_bar_button_html); + htmlButton.setChecked(false); + for (ToggleButton button : mTagToggleButtonMap.values()) { + button.setChecked(false); + } + + boolean editorHasFocus = false; + + // Add any media files that were placed in a queue due to the DOM not having loaded yet + if (mWaitingMediaFiles.size() > 0) { + // Image insertion will only work if the content field is in focus + // (for a new post, no field is in focus until user action) + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();"); + editorHasFocus = true; + + for (Map.Entry<String, MediaFile> entry : mWaitingMediaFiles.entrySet()) { + appendMediaFile(entry.getValue(), entry.getKey(), null); + } + mWaitingMediaFiles.clear(); + } + + // Add any galleries that were placed in a queue due to the DOM not having loaded yet + if (mWaitingGalleries.size() > 0) { + // Gallery insertion will only work if the content field is in focus + // (for a new post, no field is in focus until user action) + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();"); + editorHasFocus = true; + + for (MediaGallery mediaGallery : mWaitingGalleries) { + appendGallery(mediaGallery); + } + + mWaitingGalleries.clear(); + } + + if (!editorHasFocus) { + mWebView.execJavaScriptFromString("ZSSEditor.focusFirstEditableField();"); + } + + // Show the keyboard + ((InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE)) + .showSoftInput(mWebView, InputMethodManager.SHOW_IMPLICIT); + + ProfilingUtils.split("EditorFragment.onDomLoaded completed"); + ProfilingUtils.dump(); + ProfilingUtils.stop(); + } + }); + } + + public void onSelectionStyleChanged(final Map<String, Boolean> changeMap) { + mWebView.post(new Runnable() { + public void run() { + for (Map.Entry<String, Boolean> entry : changeMap.entrySet()) { + // Handle toggling format bar style buttons + ToggleButton button = mTagToggleButtonMap.get(entry.getKey()); + if (button != null) { + button.setChecked(entry.getValue()); + } + } + } + }); + } + + public void onSelectionChanged(final Map<String, String> selectionArgs) { + mFocusedFieldId = selectionArgs.get("id"); // The field now in focus + mWebView.post(new Runnable() { + @Override + public void run() { + if (!mFocusedFieldId.isEmpty()) { + switch (mFocusedFieldId) { + case "zss_field_title": + updateFormatBarEnabledState(false); + break; + case "zss_field_content": + updateFormatBarEnabledState(true); + break; + } + } + } + }); + } + + public void onMediaTapped(final String mediaId, final MediaType mediaType, final JSONObject meta, String uploadStatus) { + if (mediaType == null || !isAdded()) { + return; + } + + switch (uploadStatus) { + case "uploading": + // Display 'cancel upload' dialog + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getString(R.string.stop_upload_dialog_title)); + builder.setPositiveButton(R.string.stop_upload_button, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, true); + + mWebView.post(new Runnable() { + @Override + public void run() { + switch (mediaType) { + case IMAGE: + mWebView.execJavaScriptFromString("ZSSEditor.removeImage(" + mediaId + ");"); + break; + case VIDEO: + mWebView.execJavaScriptFromString("ZSSEditor.removeVideo(" + mediaId + ");"); + } + mUploadingMedia.remove(mediaId); + } + }); + dialog.dismiss(); + } + }); + + builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + break; + case "failed": + // Retry media upload + mEditorFragmentListener.onMediaRetryClicked(mediaId); + + mWebView.post(new Runnable() { + @Override + public void run() { + switch (mediaType) { + case IMAGE: + mWebView.execJavaScriptFromString("ZSSEditor.unmarkImageUploadFailed(" + mediaId + + ");"); + break; + case VIDEO: + mWebView.execJavaScriptFromString("ZSSEditor.unmarkVideoUploadFailed(" + mediaId + + ");"); + } + mFailedMediaIds.remove(mediaId); + mUploadingMedia.put(mediaId, mediaType); + } + }); + break; + default: + if (!mediaType.equals(MediaType.IMAGE)) { + return; + } + + // Only show image options fragment for image taps + FragmentManager fragmentManager = getFragmentManager(); + + if (fragmentManager.findFragmentByTag(ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG) != null) { + return; + } + mEditorFragmentListener.onTrackableEvent(TrackableEvent.IMAGE_EDITED); + ImageSettingsDialogFragment imageSettingsDialogFragment = new ImageSettingsDialogFragment(); + imageSettingsDialogFragment.setTargetFragment(this, + ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_REQUEST_CODE); + + Bundle dialogBundle = new Bundle(); + + dialogBundle.putString("maxWidth", mBlogSettingMaxImageWidth); + dialogBundle.putBoolean("featuredImageSupported", mFeaturedImageSupported); + + // Request and add an authorization header for HTTPS images + // Use https:// when requesting the auth header, in case the image is incorrectly using http://. + // If an auth header is returned, force https:// for the actual HTTP request. + HashMap<String, String> headerMap = new HashMap<>(); + if (mCustomHttpHeaders != null) { + headerMap.putAll(mCustomHttpHeaders); + } + + try { + final String imageSrc = meta.getString("src"); + String authHeader = mEditorFragmentListener.onAuthHeaderRequested(UrlUtils.makeHttps(imageSrc)); + if (authHeader.length() > 0) { + meta.put("src", UrlUtils.makeHttps(imageSrc)); + headerMap.put("Authorization", authHeader); + } + } catch (JSONException e) { + AppLog.e(T.EDITOR, "Could not retrieve image url from JSON metadata"); + } + dialogBundle.putSerializable("headerMap", headerMap); + + dialogBundle.putString("imageMeta", meta.toString()); + + String imageId = JSONUtils.getString(meta, "attachment_id"); + if (!imageId.isEmpty()) { + dialogBundle.putBoolean("isFeatured", mFeaturedImageId == Integer.parseInt(imageId)); + } + + imageSettingsDialogFragment.setArguments(dialogBundle); + + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + + fragmentTransaction.add(android.R.id.content, imageSettingsDialogFragment, + ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG) + .addToBackStack(null) + .commit(); + + mWebView.notifyVisibilityChanged(false); + break; + } + } + + public void onLinkTapped(String url, String title) { + LinkDialogFragment linkDialogFragment = new LinkDialogFragment(); + linkDialogFragment.setTargetFragment(this, LinkDialogFragment.LINK_DIALOG_REQUEST_CODE_UPDATE); + + Bundle dialogBundle = new Bundle(); + + dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_URL, url); + dialogBundle.putString(LinkDialogFragment.LINK_DIALOG_ARG_TEXT, title); + + linkDialogFragment.setArguments(dialogBundle); + linkDialogFragment.show(getFragmentManager(), "LinkDialogFragment"); + } + + @Override + public void onMediaRemoved(String mediaId) { + mUploadingMedia.remove(mediaId); + mFailedMediaIds.remove(mediaId); + mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, true); + } + + @Override + public void onMediaReplaced(String mediaId) { + mUploadingMedia.remove(mediaId); + } + + @Override + public void onVideoPressInfoRequested(final String videoId) { + mEditorFragmentListener.onVideoPressInfoRequested(videoId); + } + + public void onGetHtmlResponse(Map<String, String> inputArgs) { + String functionId = inputArgs.get("function"); + + if (functionId.isEmpty()) { + return; + } + + switch (functionId) { + case "getHTMLForCallback": + String fieldId = inputArgs.get("id"); + String fieldContents = inputArgs.get("contents"); + if (!fieldId.isEmpty()) { + switch (fieldId) { + case "zss_field_title": + mTitle = fieldContents; + mGetTitleCountDownLatch.countDown(); + break; + case "zss_field_content": + mContentHtml = fieldContents; + mGetContentCountDownLatch.countDown(); + break; + } + } + break; + case "getSelectedTextToLinkify": + mJavaScriptResult = inputArgs.get("result"); + mGetSelectedTextCountDownLatch.countDown(); + break; + case "getFailedMedia": + String[] mediaIds = inputArgs.get("ids").split(","); + for (String mediaId : mediaIds) { + if (!mediaId.equals("")) { + mFailedMediaIds.add(mediaId); + } + } + } + } + + public void setWebViewErrorListener(ErrorListener errorListener) { + mWebView.setErrorListener(errorListener); + } + + private void updateVisualEditorFields() { + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_title').setPlainText('" + + Utils.escapeHtml(mTitle) + "');"); + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setHTML('" + + Utils.escapeHtml(mContentHtml) + "');"); + } + + /** + * Hide the action bar if needed. + */ + private void hideActionBarIfNeeded() { + + ActionBar actionBar = getActionBar(); + if (actionBar != null + && !isHardwareKeyboardPresent() + && mHideActionBarOnSoftKeyboardUp + && mIsKeyboardOpen + && actionBar.isShowing()) { + getActionBar().hide(); + } + } + + /** + * Show the action bar if needed. + */ + private void showActionBarIfNeeded() { + + ActionBar actionBar = getActionBar(); + if (actionBar != null && !actionBar.isShowing()) { + actionBar.show(); + } + } + + /** + * Returns true if a hardware keyboard is detected, otherwise false. + */ + private boolean isHardwareKeyboardPresent() { + Configuration config = getResources().getConfiguration(); + boolean returnValue = false; + if (config.keyboard != Configuration.KEYBOARD_NOKEYS) { + returnValue = true; + } + return returnValue; + } + + void updateFormatBarEnabledState(boolean enabled) { + float alpha = (enabled ? TOOLBAR_ALPHA_ENABLED : TOOLBAR_ALPHA_DISABLED); + for(ToggleButton button : mTagToggleButtonMap.values()) { + button.setEnabled(enabled); + button.setAlpha(alpha); + } + + mIsFormatBarDisabled = !enabled; + } + + private void clearFormatBarButtons() { + for (ToggleButton button : mTagToggleButtonMap.values()) { + if (button != null) { + button.setChecked(false); + } + } + } + + private void onFormattingButtonClicked(ToggleButton toggleButton) { + String tag = toggleButton.getTag().toString(); + buttonTappedListener(toggleButton); + if (mWebView.getVisibility() == View.VISIBLE) { + mWebView.execJavaScriptFromString("ZSSEditor.set" + StringUtils.capitalize(tag) + "();"); + } else { + applyFormattingHtmlMode(toggleButton, tag); + } + } + + private void buttonTappedListener(ToggleButton toggleButton) { + int id = toggleButton.getId(); + if (id == R.id.format_bar_button_bold) { + mEditorFragmentListener.onTrackableEvent(TrackableEvent.BOLD_BUTTON_TAPPED); + } else if (id == R.id.format_bar_button_italic) { + mEditorFragmentListener.onTrackableEvent(TrackableEvent.ITALIC_BUTTON_TAPPED); + } else if (id == R.id.format_bar_button_ol) { + mEditorFragmentListener.onTrackableEvent(TrackableEvent.OL_BUTTON_TAPPED); + } else if (id == R.id.format_bar_button_ul) { + mEditorFragmentListener.onTrackableEvent(TrackableEvent.UL_BUTTON_TAPPED); + } else if (id == R.id.format_bar_button_quote) { + mEditorFragmentListener.onTrackableEvent(TrackableEvent.BLOCKQUOTE_BUTTON_TAPPED); + } else if (id == R.id.format_bar_button_strikethrough) { + mEditorFragmentListener.onTrackableEvent(TrackableEvent.STRIKETHROUGH_BUTTON_TAPPED); + } + } + + /** + * In HTML mode, applies formatting to selected text, or inserts formatting tag at current cursor position + * @param toggleButton format bar button which was clicked + * @param tag identifier tag + */ + private void applyFormattingHtmlMode(ToggleButton toggleButton, String tag) { + if (mSourceViewContent == null) { + return; + } + + // Replace style tags with their proper HTML tags + String htmlTag; + if (tag.equals(getString(R.string.format_bar_tag_bold))) { + htmlTag = "b"; + } else if (tag.equals(getString(R.string.format_bar_tag_italic))) { + htmlTag = "i"; + } else if (tag.equals(getString(R.string.format_bar_tag_strikethrough))) { + htmlTag = "del"; + } else if (tag.equals(getString(R.string.format_bar_tag_unorderedList))) { + htmlTag = "ul"; + } else if (tag.equals(getString(R.string.format_bar_tag_orderedList))) { + htmlTag = "ol"; + } else { + htmlTag = tag; + } + + int selectionStart = mSourceViewContent.getSelectionStart(); + int selectionEnd = mSourceViewContent.getSelectionEnd(); + + if (selectionStart > selectionEnd) { + int temp = selectionEnd; + selectionEnd = selectionStart; + selectionStart = temp; + } + + boolean textIsSelected = selectionEnd > selectionStart; + + String startTag = "<" + htmlTag + ">"; + String endTag = "</" + htmlTag + ">"; + + // Add li tags together with ul and ol tags + if (htmlTag.equals("ul") || htmlTag.equals("ol")) { + startTag = startTag + "\n\t<li>"; + endTag = "</li>\n" + endTag; + } + + Editable content = mSourceViewContent.getText(); + if (textIsSelected) { + // Surround selected text with opening and closing tags + content.insert(selectionStart, startTag); + content.insert(selectionEnd + startTag.length(), endTag); + toggleButton.setChecked(false); + mSourceViewContent.setSelection(selectionEnd + startTag.length() + endTag.length()); + } else if (toggleButton.isChecked()) { + // Insert opening tag + content.insert(selectionStart, startTag); + mSourceViewContent.setSelection(selectionEnd + startTag.length()); + } else { + // Insert closing tag + content.insert(selectionEnd, endTag); + mSourceViewContent.setSelection(selectionEnd + endTag.length()); + } + } + + @Override + public void onActionFinished() { + mActionStartedAt = -1; + } +} |