package org.wordpress.android.editor; 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.DialogInterface; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.Spanned; import android.text.TextUtils; import android.view.DragEvent; 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; import android.webkit.URLUtil; import com.android.volley.VolleyError; import com.android.volley.toolbox.ImageLoader; import org.jetbrains.annotations.NotNull; import org.json.JSONException; import org.json.JSONObject; import org.wordpress.android.editor.MetadataUtils.AttributesWithClass; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.DisplayUtils; import org.wordpress.android.util.ImageUtils; import org.wordpress.android.util.JSONUtils; import org.wordpress.android.util.ProfilingUtils; 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 org.wordpress.aztec.AztecAttributes; import org.wordpress.aztec.AztecText; import org.wordpress.aztec.AztecText.OnMediaTappedListener; import org.wordpress.aztec.HistoryListener; import org.wordpress.aztec.Html; import org.wordpress.aztec.source.SourceViewEditText; import org.wordpress.aztec.toolbar.AztecToolbar; import org.wordpress.aztec.toolbar.AztecToolbarClickListener; import org.xml.sax.Attributes; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; public class AztecEditorFragment extends EditorFragmentAbstract implements OnImeBackListener, EditorMediaUploadListener, OnMediaTappedListener, AztecToolbarClickListener, HistoryListener { private static final String ATTR_ALIGN_DASH = "align-"; private static final String ATTR_CLASS = "class"; private static final String ATTR_ID_WP = "data-wpid"; private static final String ATTR_IMAGE_WP_DASH = "wp-image-"; private static final String ATTR_SIZE = "size"; private static final String ATTR_SIZE_DASH = "size-"; private static final String TEMP_IMAGE_ID = "data-temp-aztec-id"; private static final int MIN_BITMAP_DIMENSION_DP = 48; public static final int MAX_ACTION_TIME_MS = 2000; private static final MediaFile DEFAULT_MEDIA = new MediaFile(); private static final int DEFAULT_MEDIA_HEIGHT = DEFAULT_MEDIA.getHeight(); private static final int DEFAULT_MEDIA_WIDTH = DEFAULT_MEDIA.getWidth(); 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"); private boolean mIsKeyboardOpen = false; private boolean mEditorWasPaused = false; private boolean mHideActionBarOnSoftKeyboardUp = false; private AztecText title; private AztecText content; private SourceViewEditText source; private AztecToolbar formattingToolbar; private Html.ImageGetter imageLoader; private Handler invalidateOptionsHandler; private Runnable invalidateOptionsRunnable; private Map<String, MediaType> mUploadingMedia; private Set<String> mFailedMediaIds; private long mActionStartedAt = -1; private ImagePredicate mTappedImagePredicate; public static AztecEditorFragment newInstance(String title, String content) { AztecEditorFragment fragment = new AztecEditorFragment(); Bundle args = new Bundle(); args.putString(ARG_PARAM_TITLE, title); args.putString(ARG_PARAM_CONTENT, content); fragment.setArguments(args); return fragment; } @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_aztec_editor, container, false); mUploadingMedia = new HashMap<>(); mFailedMediaIds = new HashSet<>(); title = (AztecText) view.findViewById(R.id.title); content = (AztecText)view.findViewById(R.id.aztec); source = (SourceViewEditText) view.findViewById(R.id.source); source.setHint("<p>" + getString(R.string.edit_hint) + "</p>"); formattingToolbar = (AztecToolbar) view.findViewById(R.id.formatting_toolbar); formattingToolbar.setEditor(content, source); formattingToolbar.setToolbarListener(this); title.setOnFocusChangeListener( new View.OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean hasFocus) { formattingToolbar.enableFormatButtons(!hasFocus); } } ); // initialize the text & HTML source.setHistory(content.getHistory()); content.setImageGetter(imageLoader); content.getHistory().setHistoryListener(this); content.setOnMediaTappedListener(this); mEditorFragmentListener.onEditorFragmentInitialized(); content.setOnDragListener(mOnDragListener); source.setOnDragListener(mOnDragListener); setHasOptionsMenu(true); invalidateOptionsHandler = new Handler(); invalidateOptionsRunnable = new Runnable() { @Override public void run() { getActivity().invalidateOptionsMenu(); } }; return view; } public void setImageLoader(Html.ImageGetter imageLoader) { this.imageLoader = imageLoader; } @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 onSaveInstanceState(Bundle outState) { outState.putCharSequence(ATTR_TITLE, getTitle()); outState.putCharSequence(ATTR_CONTENT, getContent()); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.menu_aztec, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { // TODO: disable undo/redo in media mode boolean canRedo = content.history.redoValid(); boolean canUndo = content.history.undoValid(); if (menu != null && menu.findItem(R.id.redo) != null) { menu.findItem(R.id.redo).setEnabled(canRedo); } if (menu != null && menu.findItem(R.id.undo) != null) { menu.findItem(R.id.undo).setEnabled(canUndo); } super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.undo) { if (content.getVisibility() == View.VISIBLE) { content.undo(); } else { source.undo(); } return true; } else if (item.getItemId() == R.id.redo) { if (content.getVisibility() == View.VISIBLE) { content.redo(); } else { source.redo(); } return true; } return false; } @Override public void onRedoEnabled() { invalidateOptionsHandler.removeCallbacks(invalidateOptionsRunnable); invalidateOptionsHandler.postDelayed(invalidateOptionsRunnable, getResources().getInteger(android.R.integer.config_mediumAnimTime) ); } @Override public void onUndoEnabled() { invalidateOptionsHandler.removeCallbacks(invalidateOptionsRunnable); invalidateOptionsHandler.postDelayed(invalidateOptionsRunnable, getResources().getInteger(android.R.integer.config_mediumAnimTime) ); } private ActionBar getActionBar() { if (!isAdded()) { return null; } if (getActivity() instanceof AppCompatActivity) { return ((AppCompatActivity) getActivity()).getSupportActionBar(); } else { return null; } } /** * Intercept back button press while soft keyboard is visible. */ @Override public void onImeBack() { mIsKeyboardOpen = false; showActionBarIfNeeded(); } @Override public void setTitle(CharSequence text) { title.setText(text); } @Override public void setContent(CharSequence text) { content.fromHtml(text.toString()); updateFailedMediaList(); overlayFailedMedia(); } /** * 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 ""; } // TODO: Aztec returns a ZeroWidthJoiner when empty so, strip it. Aztec needs fixing to return empty string. return StringUtils.notNullStr(title.getText().toString().replaceAll(" $", "").replaceAll("\u200B", "")); } @Override public void onToolbarHtmlModeClicked() { if (!isAdded()) { return; } checkForFailedUploadAndSwitchToHtmlMode(); } private void checkForFailedUploadAndSwitchToHtmlMode() { // Show an Alert Dialog asking the user if he wants to remove all failed media before upload if (hasFailedMediaUploads()) { new AlertDialog.Builder(getActivity()) .setMessage(R.string.editor_failed_uploads_switch_html) .setPositiveButton(R.string.editor_remove_failed_uploads, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { // Clear failed uploads and switch to HTML mode removeAllFailedMediaUploads(); toggleHtmlMode(); } }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // nothing special to do } }) .create() .show(); } else { toggleHtmlMode(); } } private void toggleHtmlMode() { mEditorFragmentListener.onTrackableEvent(TrackableEvent.HTML_BUTTON_TAPPED); // Don't switch to HTML mode if currently uploading media if (!mUploadingMedia.isEmpty() || isActionInProgress()) { ToastUtils.showToast(getActivity(), R.string.alert_action_while_uploading, ToastUtils.Duration.LONG); return; } formattingToolbar.toggleEditorMode(); if (source.getVisibility() == View.VISIBLE) { updateFailedMediaList(); } } public void enableMediaMode(boolean enable) { formattingToolbar.enableMediaMode(enable); getActivity().invalidateOptionsMenu(); } @Override public boolean isActionInProgress() { return System.currentTimeMillis() - mActionStartedAt < MAX_ACTION_TIME_MS; } private void updateFailedMediaList() { AztecText.AttributePredicate failedPredicate = new AztecText.AttributePredicate() { @Override public boolean matches(@NonNull Attributes attrs) { AttributesWithClass attributesWithClass = new AttributesWithClass(attrs); return attributesWithClass.hasClass(ATTR_STATUS_FAILED); } }; mFailedMediaIds.clear(); for (Attributes attrs : content.getAllElementAttributes(failedPredicate)) { mFailedMediaIds.add(attrs.getValue(ATTR_ID_WP)); } } private void overlayFailedMedia() { for (String localMediaId : mFailedMediaIds) { Attributes attributes = content.getElementAttributes(ImagePredicate.getLocalMediaIdPredicate(localMediaId)); overlayFailedMedia(localMediaId, attributes); } } private void overlayFailedMedia(String localMediaId, Attributes attributes) { // set intermediate shade overlay AztecText.AttributePredicate localMediaIdPredicate = ImagePredicate.getLocalMediaIdPredicate(localMediaId); content.setOverlay(localMediaIdPredicate, 0, new ColorDrawable(getResources().getColor(R.color.media_shade_overlay_error_color)), Gravity.FILL); Drawable alertDrawable = getResources().getDrawable(R.drawable.media_retry_image); content.setOverlay(localMediaIdPredicate, 1, alertDrawable, Gravity.CENTER); content.updateElementAttributes(localMediaIdPredicate, new AztecAttributes(attributes)); } /** * 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 (content.getVisibility() == View.VISIBLE) { return content.toHtml(false); } else { return source.getPureHtml(false); } } @Override public void appendMediaFile(final MediaFile mediaFile, final String mediaUrl, ImageLoader imageLoader) { final String safeMediaUrl = Utils.escapeQuotes(mediaUrl); if (URLUtil.isNetworkUrl(mediaUrl)) { if (mediaFile.isVideo()) { // TODO: insert video ToastUtils.showToast(getActivity(), R.string.media_insert_unimplemented); } else { imageLoader.get(mediaUrl, new ImageLoader.ImageListener() { @Override public void onErrorResponse(VolleyError error) { // Show failed placeholder. ToastUtils.showToast(getActivity(), R.string.error_media_load); Drawable drawable = getResources().getDrawable(R.drawable.ic_image_failed_grey_a_40_48dp); AztecAttributes attributes = new AztecAttributes(); attributes.setValue(ATTR_SRC, mediaUrl); setAttributeValuesIfNotDefault(attributes, mediaFile); content.insertMedia(drawable, attributes); } @Override public void onResponse(ImageLoader.ImageContainer container, boolean isImmediate) { Bitmap downloadedBitmap = container.getBitmap(); if (downloadedBitmap == null) { // No bitmap downloaded from server. return; } AztecAttributes attributes = new AztecAttributes(); attributes.setValue(ATTR_SRC, mediaUrl); setAttributeValuesIfNotDefault(attributes, mediaFile); int minimumDimension = DisplayUtils.dpToPx(getActivity(), MIN_BITMAP_DIMENSION_DP); if (downloadedBitmap.getHeight() < minimumDimension || downloadedBitmap.getWidth() < minimumDimension) { // Bitmap is too small. Show image placeholder. ToastUtils.showToast(getActivity(), R.string.error_media_small); Drawable drawable = getResources().getDrawable(R.drawable.ic_image_loading_grey_a_40_48dp); content.insertMedia(drawable, attributes); return; } Bitmap resizedBitmap = ImageUtils.getScaledBitmapAtLongestSide(downloadedBitmap, DisplayUtils.getDisplayPixelWidth(getActivity())); content.insertMedia(new BitmapDrawable(getResources(), resizedBitmap), attributes); } }, 0, 0); } mActionStartedAt = System.currentTimeMillis(); } else { String localMediaId = String.valueOf(mediaFile.getId()); if (mediaFile.isVideo()) { // TODO: insert local video ToastUtils.showToast(getActivity(), R.string.media_insert_unimplemented); } else { AztecAttributes attrs = new AztecAttributes(); attrs.setValue(ATTR_ID_WP, localMediaId); attrs.setValue(ATTR_SRC, safeMediaUrl); attrs.setValue(ATTR_CLASS, ATTR_STATUS_UPLOADING); // load a scaled version of the image to prevent OOM exception int maxWidth = DisplayUtils.getDisplayPixelWidth(getActivity()); Bitmap bitmapToShow = ImageUtils.getWPImageSpanThumbnailFromFilePath(getActivity(), safeMediaUrl, maxWidth); if (bitmapToShow != null) { content.insertMedia(new BitmapDrawable(getResources(), bitmapToShow), attrs); } else { // Failed to retrieve bitmap. Show failed placeholder. ToastUtils.showToast(getActivity(), R.string.error_media_load); Drawable drawable = getResources().getDrawable(R.drawable.ic_image_failed_grey_a_40_48dp); drawable.setBounds(0, 0, maxWidth, maxWidth); content.insertMedia(drawable, attrs); } // set intermediate shade overlay AztecText.AttributePredicate localMediaIdPredicate = ImagePredicate.getLocalMediaIdPredicate(localMediaId); content.setOverlay(localMediaIdPredicate, 0, new ColorDrawable(getResources().getColor(R.color.media_shade_overlay_color)), Gravity.FILL); Drawable progressDrawable = getResources().getDrawable(android.R.drawable.progress_horizontal); // set the height of the progress bar to 2 (it's in dp since the drawable will be adjusted by the span) progressDrawable.setBounds(0, 0, 0, 4); content.setOverlay(localMediaIdPredicate, 1, progressDrawable, Gravity.FILL_HORIZONTAL | Gravity.TOP); content.updateElementAttributes(localMediaIdPredicate, attrs); content.refreshText(); mUploadingMedia.put(localMediaId, MediaType.IMAGE); } } } @Override public void appendGallery(MediaGallery mediaGallery) { ToastUtils.showToast(getActivity(), R.string.media_insert_unimplemented); } @Override public void setUrlForVideoPressId(final String videoId, final String videoUrl, final String posterUrl) { } @Override public boolean isUploadingMedia() { return (mUploadingMedia.size() > 0); } @Override public boolean hasFailedMediaUploads() { return (mFailedMediaIds.size() > 0); } @Override public void removeAllFailedMediaUploads() { content.removeMedia(new AztecText.AttributePredicate() { @Override public boolean matches(@NotNull Attributes attrs) { return new AttributesWithClass(attrs).hasClass(ATTR_STATUS_FAILED); } }); } @Override public Spanned getSpannedContent() { return null; } @Override public void setTitlePlaceholder(CharSequence placeholderText) { } @Override public void setContentPlaceholder(CharSequence placeholderText) { } @Override public void onMediaUploadSucceeded(final String localMediaId, final MediaFile mediaFile) { if(!isAdded()) { return; } final MediaType mediaType = mUploadingMedia.get(localMediaId); if (mediaType != null) { String remoteUrl = Utils.escapeQuotes(mediaFile.getFileURL()); if (mediaType.equals(MediaType.IMAGE)) { AztecAttributes attrs = new AztecAttributes(); attrs.setValue(ATTR_SRC, remoteUrl); // clear overlay ImagePredicate predicate = ImagePredicate.getLocalMediaIdPredicate(localMediaId); content.clearOverlays(predicate); content.updateElementAttributes(predicate, attrs); content.refreshText(); mUploadingMedia.remove(localMediaId); } else if (mediaType.equals(MediaType.VIDEO)) { // TODO: update video element } } } private static class ImagePredicate implements AztecText.AttributePredicate { private final String mId; private final String mAttributeName; static ImagePredicate getLocalMediaIdPredicate(String id) { return new ImagePredicate(id, ATTR_ID_WP); } ImagePredicate(String id, String attributeName) { mId = id; mAttributeName = attributeName; } @Override public boolean matches(@NotNull Attributes attrs) { return attrs.getIndex(mAttributeName) > -1 && attrs.getValue(mAttributeName).equals(mId); } } @Override public void onMediaUploadProgress(final String localMediaId, final float progress) { if(!isAdded()) { return; } final MediaType mediaType = mUploadingMedia.get(localMediaId); if (mediaType != null) { AztecText.AttributePredicate localMediaIdPredicate = ImagePredicate.getLocalMediaIdPredicate(localMediaId); content.setOverlayLevel(localMediaIdPredicate, 1, (int)(progress * 10000)); content.refreshText(); } } @Override public void onMediaUploadFailed(final String localMediaId, final String errorMessage) { if(!isAdded()) { return; } MediaType mediaType = mUploadingMedia.get(localMediaId); if (mediaType != null) { switch (mediaType) { case IMAGE: AttributesWithClass attributesWithClass = new AttributesWithClass( content.getElementAttributes(ImagePredicate.getLocalMediaIdPredicate(localMediaId))); attributesWithClass.removeClass(ATTR_STATUS_UPLOADING); attributesWithClass.addClass(ATTR_STATUS_FAILED); overlayFailedMedia(localMediaId, attributesWithClass.getAttributes()); content.refreshText(); break; case VIDEO: // TODO: mark media as upload-failed } mFailedMediaIds.add(localMediaId); mUploadingMedia.remove(localMediaId); } } @Override public void onGalleryMediaUploadSucceeded(final long galleryId, long remoteMediaId, int remaining) { } /** * 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; } private final View.OnDragListener mOnDragListener = new View.OnDragListener() { 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()); content.setSelection(content.getOffsetForPosition(x, y)); break; case DragEvent.ACTION_DRAG_EXITED: // clear any drop marking maybe break; case DragEvent.ACTION_DROP: if (source.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) && isTitleFocused()) { // 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) { content.getText().insert(content.getSelectionStart(), reformatVisually(Utils.escapeHtml(text))); } else { ToastUtils.showToast(getActivity(), R.string.editor_dropped_text_error, ToastUtils.Duration.SHORT); AppLog.d(AppLog.T.EDITOR, "Dropped text was null!"); } } private String reformatVisually(String text) { // TODO: implement wp.loadText (see wpload.js) return text; } private boolean isTitleFocused() { return title.isFocused(); } }; /** * Save post content from source HTML. */ public void saveContentFromSource() { if (content != null && source != null && source.getVisibility() == View.VISIBLE) { content.fromHtml(source.getPureHtml(false)); } } @Override public void onToolbarAddMediaClicked() { mEditorFragmentListener.onTrackableEvent(TrackableEvent.MEDIA_BUTTON_TAPPED); if (isActionInProgress()) { ToastUtils.showToast(getActivity(), R.string.alert_action_while_uploading, ToastUtils.Duration.LONG); return; } if (source.isFocused()) { ToastUtils.showToast(getActivity(), R.string.alert_insert_image_html_mode, ToastUtils.Duration.LONG); } else { mEditorFragmentListener.onAddMediaClicked(); } } @Override public void mediaTapped(@NotNull AztecAttributes attrs, int naturalWidth, int naturalHeight) { Set<String> classes = MetadataUtils.getClassAttribute(attrs); String idName; String uploadStatus = ""; JSONObject meta = MetadataUtils.getMetadata(new AttributesWithClass(attrs), naturalWidth, naturalHeight); if (classes.contains(ATTR_STATUS_UPLOADING)) { uploadStatus = ATTR_STATUS_UPLOADING; idName = ATTR_ID_WP; } else if (classes.contains(ATTR_STATUS_FAILED)) { uploadStatus = ATTR_STATUS_FAILED; idName = ATTR_ID_WP; } else { idName = ATTR_ID; } String id = attrs.getValue(idName); // generate the element ID if ATTR_ID or ATTR_ID_WP are missing if (!attrs.hasAttribute(idName) || TextUtils.isEmpty(attrs.getValue(idName))) { idName = TEMP_IMAGE_ID; id = UUID.randomUUID().toString(); } attrs.setValue(idName, id); mTappedImagePredicate = new ImagePredicate(id, idName); onMediaTapped(id, MediaType.IMAGE, meta, uploadStatus); } public void onMediaTapped(final String localMediaId, final MediaType mediaType, final JSONObject meta, String uploadStatus) { if (mediaType == null || !isAdded()) { return; } switch (uploadStatus) { case ATTR_STATUS_UPLOADING: // Display 'cancel upload' dialog AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(getString(R.string.stop_upload_dialog_title)); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { if (mUploadingMedia.containsKey(localMediaId)) { mEditorFragmentListener.onMediaUploadCancelClicked(localMediaId, true); switch (mediaType) { case IMAGE: content.removeMedia(mTappedImagePredicate); break; case VIDEO: // TODO: remove video } mUploadingMedia.remove(localMediaId); } else { ToastUtils.showToast(getActivity(), R.string.upload_finished_toast).show(); } 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 ATTR_STATUS_FAILED: // Retry media upload if (mFailedMediaIds.contains(localMediaId)) { mEditorFragmentListener.onMediaRetryClicked(localMediaId); } switch (mediaType) { case IMAGE: AttributesWithClass attributesWithClass = new AttributesWithClass( content.getElementAttributes(mTappedImagePredicate)); attributesWithClass.removeClass(ATTR_STATUS_FAILED); // set intermediate shade overlay content.setOverlay(mTappedImagePredicate, 0, new ColorDrawable(getResources().getColor(R.color.media_shade_overlay_color)), Gravity.FILL); Drawable progressDrawable = getResources().getDrawable(android.R.drawable.progress_horizontal); // set the height of the progress bar to 2 (it's in dp since the drawable will be adjusted by the span) progressDrawable.setBounds(0, 0, 0, 4); content.setOverlay(mTappedImagePredicate, 1, progressDrawable, Gravity.FILL_HORIZONTAL | Gravity.TOP); content.updateElementAttributes(mTappedImagePredicate, attributesWithClass.getAttributes()); content.refreshText(); break; case VIDEO: // TODO: unmark video failed } mFailedMediaIds.remove(localMediaId); mUploadingMedia.put(localMediaId, 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.setImageLoader(mImageLoader); imageSettingsDialogFragment.setTargetFragment(this, ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_REQUEST_CODE); Bundle dialogBundle = new Bundle(); dialogBundle.putString(EXTRA_MAX_WIDTH, mBlogSettingMaxImageWidth); dialogBundle.putBoolean(EXTRA_IMAGE_FEATURED, mFeaturedImageSupported); dialogBundle.putBoolean(EXTRA_ENABLED_AZTEC, true); try { // 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 final String imageSrc = meta.getString(ATTR_SRC); String authHeader = mEditorFragmentListener.onAuthHeaderRequested(UrlUtils.makeHttps(imageSrc)); if (authHeader.length() > 0) { meta.put(ATTR_SRC, UrlUtils.makeHttps(imageSrc)); } } catch (JSONException e) { AppLog.e(AppLog.T.EDITOR, "Could not retrieve image url from JSON metadata"); } dialogBundle.putString(EXTRA_IMAGE_META, meta.toString()); String imageId = JSONUtils.getString(meta, ATTR_ID_ATTACHMENT); if (!imageId.isEmpty()) { dialogBundle.putBoolean(EXTRA_FEATURED, 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(); break; } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_REQUEST_CODE) { if (mTappedImagePredicate != null) { AztecAttributes attributes = content.getElementAttributes(mTappedImagePredicate); attributes.removeAttribute(TEMP_IMAGE_ID); content.updateElementAttributes(mTappedImagePredicate, attributes); if (data == null || data.getExtras() == null) { return; } Bundle extras = data.getExtras(); JSONObject meta; try { meta = new JSONObject(StringUtils.notNullStr(extras.getString(EXTRA_IMAGE_META))); } catch (JSONException e) { return; } attributes.setValue(ATTR_SRC, JSONUtils.getString(meta, ATTR_SRC)); if (!TextUtils.isEmpty(JSONUtils.getString(meta, ATTR_TITLE))) { attributes.setValue(ATTR_TITLE, JSONUtils.getString(meta, ATTR_TITLE)); } attributes.setValue(ATTR_DIMEN_WIDTH, JSONUtils.getString(meta, ATTR_DIMEN_WIDTH)); attributes.setValue(ATTR_DIMEN_HEIGHT, JSONUtils.getString(meta, ATTR_DIMEN_HEIGHT)); if (!TextUtils.isEmpty(JSONUtils.getString(meta, ATTR_ALT))) { attributes.setValue(ATTR_ALT, JSONUtils.getString(meta, ATTR_ALT)); } AttributesWithClass attributesWithClass = new AttributesWithClass(attributes); // remove previously set class attributes to add updated values attributesWithClass.removeClassStartingWith(ATTR_ALIGN_DASH); attributesWithClass.removeClassStartingWith(ATTR_SIZE_DASH); attributesWithClass.removeClassStartingWith(ATTR_IMAGE_WP_DASH); // only add align attribute if there is no caption since alignment is sent with shortcode if (!TextUtils.isEmpty(JSONUtils.getString(meta, ATTR_ALIGN)) && TextUtils.isEmpty(JSONUtils.getString(meta, ATTR_CAPTION))) { attributesWithClass.addClass(ATTR_ALIGN_DASH + JSONUtils.getString(meta, ATTR_ALIGN)); } if (!TextUtils.isEmpty(JSONUtils.getString(meta, ATTR_SIZE))) { attributesWithClass.addClass(ATTR_SIZE_DASH + JSONUtils.getString(meta, ATTR_SIZE)); } if (!TextUtils.isEmpty(JSONUtils.getString(meta, ATTR_ID_ATTACHMENT))) { attributesWithClass.addClass(ATTR_IMAGE_WP_DASH + JSONUtils.getString(meta, ATTR_ID_ATTACHMENT)); } // TODO: Add shortcode support to allow captions. // https://github.com/wordpress-mobile/AztecEditor-Android/issues/17 // String caption = JSONUtils.getString(meta, ATTR_CAPTION); // TODO: Fix issue with image inside link. // https://github.com/wordpress-mobile/AztecEditor-Android/issues/196 // String link = JSONUtils.getString(meta, ATTR_URL_LINK); final int imageRemoteId = extras.getInt(ATTR_ID_IMAGE_REMOTE); final boolean isFeaturedImage = extras.getBoolean(EXTRA_FEATURED); 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); } } } mTappedImagePredicate = null; } } } protected void setAttributeValuesIfNotDefault(AztecAttributes attributes, MediaFile mediaFile) { if (mediaFile.getWidth() != DEFAULT_MEDIA_WIDTH) { attributes.setValue(ATTR_DIMEN_WIDTH, String.valueOf(mediaFile.getWidth())); } if (mediaFile.getHeight() != DEFAULT_MEDIA_HEIGHT) { attributes.setValue(ATTR_DIMEN_HEIGHT, String.valueOf(mediaFile.getHeight())); } } }