diff --git a/.travis.yml b/.travis.yml index b3c1e87794b3..c065b2d6d317 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,9 @@ android: components: - extra-android-m2repository - extra-android-support - - build-tools-23.0.1 + - platform-tools + - tools + - build-tools-23.0.2 - android-23 env: diff --git a/WordPressEditor/build.gradle b/WordPressEditor/build.gradle index badd6aa00478..4dc82f05e1ee 100644 --- a/WordPressEditor/build.gradle +++ b/WordPressEditor/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.1' + classpath 'com.android.tools.build:gradle:1.5.0' } } @@ -20,11 +20,11 @@ android { publishNonDefault true compileSdkVersion 23 - buildToolsVersion "23.0.1" + buildToolsVersion "23.0.2" defaultConfig { - versionCode 3 - versionName "0.3" + versionCode 4 + versionName "0.4" minSdkVersion 14 targetSdkVersion 23 } @@ -46,8 +46,8 @@ android { } dependencies { - compile 'com.android.support:appcompat-v7:23.0.1' - compile 'com.android.support:support-v4:23.0.1' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:support-v4:23.1.1' compile 'org.wordpress:analytics:1.0.0' compile 'org.wordpress:utils:1.6.0' diff --git a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java index 199341dc45d1..96e93382b662 100644 --- a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java +++ b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java @@ -52,7 +52,12 @@ public void onMediaRetryClicked(String mediaId) { } @Override - public void onMediaUploadCancelClicked(String mediaId) { + public void onMediaUploadCancelClicked(String mediaId, boolean delete) { + + } + + @Override + public void onFeaturedImageChanged(int mediaId) { } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java index ecfeeebfa087..22ec71085260 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java @@ -30,8 +30,10 @@ import com.android.volley.toolbox.ImageLoader; +import org.json.JSONObject; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; +import org.wordpress.android.util.JSONUtils; import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.helpers.MediaFile; @@ -40,8 +42,10 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +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; @@ -77,11 +81,14 @@ public class EditorFragment extends EditorFragmentAbstract implements View.OnCli private String mTitlePlaceholder = ""; private String mContentPlaceholder = ""; + private boolean mDomHasLoaded = false; private boolean mIsKeyboardOpen = false; private boolean mEditorWasPaused = false; private boolean mHideActionBarOnSoftKeyboardUp = false; + private ConcurrentHashMap mWaitingMediaFiles; private Set mUploadingMediaIds; + private Set mFailedMediaIds; private String mJavaScriptResult = ""; @@ -118,7 +125,9 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mHideActionBarOnSoftKeyboardUp = true; } + mWaitingMediaFiles = new ConcurrentHashMap<>(); mUploadingMediaIds = new HashSet<>(); + mFailedMediaIds = new HashSet<>(); // -- WebView configuration @@ -206,6 +215,10 @@ public void onResume() { @Override public void onDetach() { + // Soft cancel (delete flag off) all media uploads currently in progress + for (String mediaId : mUploadingMediaIds) { + mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, false); + } super.onDetach(); } @@ -529,6 +542,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } final String imageMeta = extras.getString("imageMeta"); + final int imageRemoteId = extras.getInt("imageRemoteId"); + final boolean isFeaturedImage = extras.getBoolean("isFeatured"); mWebView.post(new Runnable() { @Override @@ -536,6 +551,19 @@ 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); + } + } + } } } @@ -595,7 +623,7 @@ public CharSequence getTitle() { return ""; } - if (mSourceView.getVisibility() == View.VISIBLE) { + if (mSourceView != null && mSourceView.getVisibility() == View.VISIBLE) { mTitle = mSourceViewTitle.getText().toString(); return StringUtils.notNullStr(mTitle); } @@ -634,7 +662,7 @@ public CharSequence getContent() { return ""; } - if (mSourceView.getVisibility() == View.VISIBLE) { + if (mSourceView != null && mSourceView.getVisibility() == View.VISIBLE) { mContentHtml = mSourceViewContent.getText().toString(); return StringUtils.notNullStr(mContentHtml); } @@ -665,11 +693,19 @@ public void run() { @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; + } + mWebView.post(new Runnable() { @Override public void run() { if (URLUtil.isNetworkUrl(mediaUrl)) { - mWebView.execJavaScriptFromString("ZSSEditor.insertImage('" + mediaUrl + "');"); + String mediaId = mediaFile.getMediaId(); + mWebView.execJavaScriptFromString("ZSSEditor.insertImage('" + mediaUrl + "', '" + mediaId + "');"); } else { String id = mediaFile.getMediaId(); mWebView.execJavaScriptFromString("ZSSEditor.insertLocalImage(" + id + ", '" + mediaUrl + "');"); @@ -685,6 +721,11 @@ public void appendGallery(MediaGallery mediaGallery) { // TODO } + @Override + public boolean hasFailedMediaUploads() { + return (mFailedMediaIds.size() > 0); + } + @Override public Spanned getSpannedContent() { return null; @@ -701,12 +742,12 @@ public void setContentPlaceholder(CharSequence placeholderText) { } @Override - public void onMediaUploadSucceeded(final String mediaId, final String remoteUrl) { + public void onMediaUploadSucceeded(final String mediaId, final String remoteId, final String remoteUrl) { mWebView.post(new Runnable() { @Override public void run() { mWebView.execJavaScriptFromString("ZSSEditor.replaceLocalImageWithRemoteImage(" + mediaId + ", '" + - remoteUrl + "');"); + remoteId + "', '" + remoteUrl + "');"); mUploadingMediaIds.remove(mediaId); } }); @@ -717,7 +758,7 @@ public void onMediaUploadProgress(final String mediaId, final float progress) { mWebView.post(new Runnable() { @Override public void run() { - String progressString = String.format("%.1f", progress); + String progressString = String.format(Locale.US, "%.1f", progress); mWebView.execJavaScriptFromString("ZSSEditor.setProgressOnImage(" + mediaId + ", " + progressString + ");"); } @@ -730,6 +771,7 @@ public void onMediaUploadFailed(final String mediaId) { @Override public void run() { mWebView.execJavaScriptFromString("ZSSEditor.markImageUploadFailed(" + mediaId + ");"); + mFailedMediaIds.add(mediaId); mUploadingMediaIds.remove(mediaId); } }); @@ -738,6 +780,8 @@ public void run() { public void onDomLoaded() { mWebView.post(new Runnable() { public void run() { + mDomHasLoaded = true; + mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').setMultiline('true');"); // Set title and content placeholder text @@ -748,6 +792,14 @@ public void run() { // 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.markAllUploadingImagesAsFailed();"); + + // Update the list of failed media uploads + mWebView.execJavaScriptFromString("ZSSEditor.getFailedImages();"); + hideActionBarIfNeeded(); // Reset all format bar buttons (in case they remained active through activity re-creation) @@ -756,6 +808,18 @@ public void run() { for (ToggleButton button : mTagToggleButtonMap.values()) { button.setChecked(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();"); + + for (Map.Entry entry : mWaitingMediaFiles.entrySet()) { + appendMediaFile(entry.getValue(), entry.getKey(), null); + } + mWaitingMediaFiles.clear(); + } } }); } @@ -793,7 +857,7 @@ public void run() { }); } - public void onMediaTapped(final String mediaId, String url, final String meta, String uploadStatus) { + public void onMediaTapped(final String mediaId, String url, final JSONObject meta, String uploadStatus) { switch (uploadStatus) { case "uploading": // Display 'cancel upload' dialog @@ -801,7 +865,7 @@ public void onMediaTapped(final String mediaId, String url, final String meta, S 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); + mEditorFragmentListener.onMediaUploadCancelClicked(mediaId, true); mWebView.post(new Runnable() { @Override @@ -832,6 +896,7 @@ public void onClick(DialogInterface dialog, int id) { public void run() { mWebView.execJavaScriptFromString("ZSSEditor.unmarkImageUploadFailed(" + mediaId + ");"); mWebView.execJavaScriptFromString("ZSSEditor.setProgressOnImage(" + mediaId + ", " + 0 + ");"); + mFailedMediaIds.remove(mediaId); mUploadingMediaIds.add(mediaId); } }); @@ -850,8 +915,14 @@ public void run() { Bundle dialogBundle = new Bundle(); - dialogBundle.putString("imageMeta", meta); + dialogBundle.putString("imageMeta", meta.toString()); dialogBundle.putString("maxWidth", mBlogSettingMaxImageWidth); + dialogBundle.putBoolean("featuredImageSupported", mFeaturedImageSupported); + + String imageId = JSONUtils.getString(meta, "attachment_id"); + if (!imageId.isEmpty()) { + dialogBundle.putBoolean("isFeatured", mFeaturedImageId == Integer.parseInt(imageId)); + } imageSettingsDialogFragment.setArguments(dialogBundle); @@ -907,6 +978,13 @@ public void onGetHtmlResponse(Map inputArgs) { mJavaScriptResult = inputArgs.get("result"); mGetSelectedTextCountDownLatch.countDown(); break; + case "getFailedImages": + String[] mediaIds = inputArgs.get("ids").split(","); + for (String mediaId : mediaIds) { + if (!mediaId.equals("")) { + mFailedMediaIds.add(mediaId); + } + } } } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java index a674a32aec73..2e8b032c8370 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java @@ -18,6 +18,7 @@ public abstract class EditorFragmentAbstract extends Fragment { public abstract CharSequence getContent(); public abstract void appendMediaFile(MediaFile mediaFile, String imageUrl, ImageLoader imageLoader); public abstract void appendGallery(MediaGallery mediaGallery); + public abstract boolean hasFailedMediaUploads(); public abstract void setTitlePlaceholder(CharSequence text); public abstract void setContentPlaceholder(CharSequence text); @@ -29,6 +30,7 @@ public abstract class EditorFragmentAbstract extends Fragment { protected EditorFragmentListener mEditorFragmentListener; protected boolean mFeaturedImageSupported; + protected int mFeaturedImageId; protected String mBlogSettingMaxImageWidth; protected ImageLoader mImageLoader; protected boolean mDebugModeEnabled; @@ -77,6 +79,10 @@ public void setBlogSettingMaxImageWidth(String blogSettingMaxImageWidth) { mBlogSettingMaxImageWidth = blogSettingMaxImageWidth; } + public void setFeaturedImageId(int featuredImageId) { + mFeaturedImageId = featuredImageId; + } + public void setDebugModeEnabled(boolean debugModeEnabled) { mDebugModeEnabled = debugModeEnabled; } @@ -105,9 +111,9 @@ public interface EditorFragmentListener { void onSettingsClicked(); void onAddMediaClicked(); void onMediaRetryClicked(String mediaId); - void onMediaUploadCancelClicked(String mediaId); - // TODO: remove saveMediaFile, it's currently needed for the legacy editor - we should have something like - // "EditorFragmentAbstract.getFeaturedImage()" returning the remote id + void onMediaUploadCancelClicked(String mediaId, boolean delete); + void onFeaturedImageChanged(int mediaId); + // TODO: remove saveMediaFile, it's currently needed for the legacy editor void saveMediaFile(MediaFile mediaFile); } } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java index a8a8400d123e..6f052fac2b30 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java @@ -1,7 +1,7 @@ package org.wordpress.android.editor; public interface EditorMediaUploadListener { - void onMediaUploadSucceeded(String localId, String remoteUrl); + void onMediaUploadSucceeded(String localId, String remoteId, String remoteUrl); void onMediaUploadProgress(String localId, float progress); void onMediaUploadFailed(String localId); } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java index aedb8d7e1059..ff6c0a99c84b 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java @@ -2,11 +2,13 @@ import android.app.DialogFragment; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.ActionBar; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -26,9 +28,17 @@ import org.json.JSONException; import org.json.JSONObject; import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.ToastUtils; import java.util.Arrays; - +import java.util.Locale; + +/** + * A full-screen DialogFragment with image settings. + * + * Modifies the action bar - host activity must call {@link ImageSettingsDialogFragment#dismissFragment()} + * when the fragment is dismissed to restore it. + */ public class ImageSettingsDialogFragment extends DialogFragment { public static final int IMAGE_SETTINGS_DIALOG_REQUEST_CODE = 5; @@ -45,6 +55,9 @@ public class ImageSettingsDialogFragment extends DialogFragment { private Spinner mAlignmentSpinner; private EditText mLinkTo; private EditText mWidthText; + private CheckBox mFeaturedCheckBox; + + private boolean mIsFeatured; private CharSequence mPreviousActionBarTitle; private boolean mPreviousHomeAsUpEnabled; @@ -83,29 +96,30 @@ public void onCreate(Bundle savedInstanceState) { actionBar.getCustomView().findViewById(R.id.menu_save).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - try { - mImageMeta.put("title", mTitleText.getText().toString()); - mImageMeta.put("caption", mCaptionText.getText().toString()); - mImageMeta.put("alt", mAltText.getText().toString()); - mImageMeta.put("align", mAlignmentSpinner.getSelectedItem().toString()); - mImageMeta.put("linkUrl", mLinkTo.getText().toString()); - - int newWidth = getEditTextIntegerClamped(mWidthText, 10, mMaxImageWidth); - mImageMeta.put("width", newWidth); - mImageMeta.put("height", getRelativeHeightFromWidth(newWidth)); - - // TODO: Featured image handling + mImageMeta = extractMetaDataFromFields(mImageMeta); + String imageRemoteId = ""; + try { + imageRemoteId = mImageMeta.getString("attachment_id"); } catch (JSONException e) { - AppLog.d(AppLog.T.EDITOR, "Unable to update JSON array"); + AppLog.e(AppLog.T.EDITOR, "Unable to retrieve featured image id from meta data"); } Intent intent = new Intent(); intent.putExtra("imageMeta", mImageMeta.toString()); + + mIsFeatured = mFeaturedCheckBox.isChecked(); + intent.putExtra("isFeatured", mIsFeatured); + + if (!imageRemoteId.isEmpty()) { + intent.putExtra("imageRemoteId", Integer.parseInt(imageRemoteId)); + } + getTargetFragment().onActivityResult(getTargetRequestCode(), getTargetRequestCode(), intent); restorePreviousActionBar(); getFragmentManager().popBackStack(); + ToastUtils.showToast(getActivity(), R.string.image_settings_save_toast); } }); } @@ -123,8 +137,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mLinkTo = (EditText) view.findViewById(R.id.image_link_to); SeekBar widthSeekBar = (SeekBar) view.findViewById(R.id.image_width_seekbar); mWidthText = (EditText) view.findViewById(R.id.image_width_text); - final CheckBox featuredCheckBox = (CheckBox) view.findViewById(R.id.featuredImage); - final CheckBox featuredInPostCheckBox = (CheckBox) view.findViewById(R.id.featuredInPost); + mFeaturedCheckBox = (CheckBox) view.findViewById(R.id.featuredImage); // Populate the dialog with existing values Bundle bundle = getArguments(); @@ -143,6 +156,10 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mAltText.setText(mImageMeta.getString("alt")); String alignment = mImageMeta.getString("align"); + + // Capitalize the alignment value to match the spinner entries + alignment = alignment.substring(0, 1).toUpperCase(Locale.US) + alignment.substring(1); + String[] alignmentArray = getResources().getStringArray(R.array.alignment_array); mAlignmentSpinner.setSelection(Arrays.asList(alignmentArray).indexOf(alignment)); @@ -152,7 +169,12 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa setupWidthSeekBar(widthSeekBar, mWidthText, mImageMeta.getInt("width")); - // TODO: Featured image handling + boolean featuredImageSupported = bundle.getBoolean("featuredImageSupported"); + if (featuredImageSupported) { + mFeaturedCheckBox.setVisibility(View.VISIBLE); + mIsFeatured = bundle.getBoolean("isFeatured", false); + mFeaturedCheckBox.setChecked(mIsFeatured); + } } catch (JSONException e1) { AppLog.d(AppLog.T.EDITOR, "Missing JSON properties"); @@ -184,8 +206,7 @@ public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { - restorePreviousActionBar(); - getFragmentManager().popBackStack(); + dismissFragment(); return true; } return super.onOptionsItemSelected(item); @@ -203,20 +224,95 @@ private ActionBar getActionBar() { } } + /** + * To be called when the fragment is being dismissed, either by ActionBar navigation or by pressing back in the + * navigation bar. + * Displays a confirmation dialog if there are unsaved changes, otherwise undoes the fragment's modifications to + * the ActionBar and restores the last visible fragment. + */ + public void dismissFragment() { + try { + JSONObject newImageMeta = extractMetaDataFromFields(new JSONObject()); + + for (int i = 0; i < newImageMeta.names().length(); i++) { + String name = newImageMeta.names().getString(i); + if (!newImageMeta.getString(name).equals(mImageMeta.getString(name))) { + showDiscardChangesDialog(); + return; + } + } + + if (mFeaturedCheckBox.isChecked() != mIsFeatured) { + // Featured image status has changed + showDiscardChangesDialog(); + return; + } + } catch (JSONException e) { + AppLog.d(AppLog.T.EDITOR, "Unable to update JSON array"); + } + + restorePreviousActionBar(); + getFragmentManager().popBackStack(); + } + private void restorePreviousActionBar() { ActionBar actionBar = getActionBar(); if (actionBar != null) { actionBar.setTitle(mPreviousActionBarTitle); actionBar.setHomeAsUpIndicator(null); actionBar.setDisplayHomeAsUpEnabled(mPreviousHomeAsUpEnabled); - if (mPreviousCustomView != null) { - actionBar.setCustomView(mPreviousCustomView); - } else { + + actionBar.setCustomView(mPreviousCustomView); + if (mPreviousCustomView == null) { actionBar.setDisplayShowCustomEnabled(false); } } } + /** + * Displays a dialog asking the user to confirm that they want to exit, discarding unsaved changes. + */ + private void showDiscardChangesDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getString(R.string.image_settings_dismiss_dialog_title)); + builder.setPositiveButton(getString(R.string.discard), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + restorePreviousActionBar(); + getFragmentManager().popBackStack(); + } + }); + + builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + + /** + * Extracts the meta data from the dialog fields and updates the entries in the given JSONObject. + */ + private JSONObject extractMetaDataFromFields(JSONObject metaData) { + try { + metaData.put("title", mTitleText.getText().toString()); + metaData.put("caption", mCaptionText.getText().toString()); + metaData.put("alt", mAltText.getText().toString()); + metaData.put("align", mAlignmentSpinner.getSelectedItem().toString().toLowerCase(Locale.US)); + metaData.put("linkUrl", mLinkTo.getText().toString()); + + int newWidth = getEditTextIntegerClamped(mWidthText, 10, mMaxImageWidth); + metaData.put("width", newWidth); + metaData.put("height", getRelativeHeightFromWidth(newWidth)); + } catch (JSONException e) { + AppLog.d(AppLog.T.EDITOR, "Unable to build JSON object from new meta data"); + } + + return metaData; + } + private void loadThumbnail(final String src, final ImageView thumbnailImage) { Thread thread = new Thread(new Runnable() { @Override @@ -224,12 +320,14 @@ public void run() { if (isAdded()) { final Uri localUri = Utils.downloadExternalMedia(getActivity(), Uri.parse(src)); - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - thumbnailImage.setImageURI(localUri); - } - }); + if (getActivity() != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + thumbnailImage.setImageURI(localUri); + } + }); + } } } }); diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java index 7cac60a45944..5a56ff530e27 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java @@ -113,11 +113,14 @@ public void executeCallback(String callbackId, String params) { } String mediaMeta = mediaDataMap.get("meta"); + JSONObject mediaMetaJson = new JSONObject(); + if (mediaMeta != null) { mediaMeta = Utils.decodeHtml(mediaMeta); try { - String classes = JSONUtils.getString(new JSONObject(mediaMeta), "classes"); + mediaMetaJson = new JSONObject(mediaMeta); + String classes = JSONUtils.getString(mediaMetaJson, "classes"); Set classesSet = Utils.splitDelimitedString(classes, ", "); if (classesSet.contains("uploading")) { @@ -131,7 +134,7 @@ public void executeCallback(String callbackId, String params) { } } - mListener.onMediaTapped(mediaId, mediaUrl, mediaMeta, uploadStatus); + mListener.onMediaTapped(mediaId, mediaUrl, mediaMetaJson, uploadStatus); break; case CALLBACK_LINK_TAP: // Extract and HTML-decode the link data from the callback params @@ -175,6 +178,8 @@ public void executeCallback(String callbackId, String params) { case "getSelectedText": responseIds.add("result"); break; + case "getFailedImages": + responseIds.add("ids"); } responseDataSet = Utils.splitValuePairDelimitedString(params, JS_CALLBACK_DELIMITER, responseIds); diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java index f97b5488f14b..eb3747dfcf3e 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java @@ -1130,6 +1130,11 @@ public void appendGallery(MediaGallery mediaGallery) { editableText.insert(selectionEnd + 1, "\n\n"); } + @Override + public boolean hasFailedMediaUploads() { + return false; + } + @Override public void setTitlePlaceholder(CharSequence text) { diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java index fc035fff1565..89941ba79ead 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java @@ -1,12 +1,14 @@ package org.wordpress.android.editor; +import org.json.JSONObject; + import java.util.Map; public interface OnJsEditorStateChangedListener { void onDomLoaded(); void onSelectionChanged(Map selectionArgs); void onSelectionStyleChanged(Map changeSet); - void onMediaTapped(String mediaId, String url, String meta, String uploadStatus); + void onMediaTapped(String mediaId, String url, JSONObject meta, String uploadStatus); void onLinkTapped(String url, String title); void onGetHtmlResponse(Map responseArgs); } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java index c0ea76585233..e02f98eb2745 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/legacy/EditLinkActivity.java @@ -3,13 +3,14 @@ import android.app.Activity; import android.content.Intent; import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; import android.widget.EditText; import org.wordpress.android.editor.R; -public class EditLinkActivity extends Activity { +public class EditLinkActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/WordPressEditor/src/main/res/layout/dialog_image_options.xml b/WordPressEditor/src/main/res/layout/dialog_image_options.xml index 7ee3e2ca203b..9b471f50832f 100644 --- a/WordPressEditor/src/main/res/layout/dialog_image_options.xml +++ b/WordPressEditor/src/main/res/layout/dialog_image_options.xml @@ -148,15 +148,6 @@ android:text="@string/featured" android:layout_marginStart="@dimen/image_settings_dialog_input_field_start_margin" android:layout_marginLeft="@dimen/image_settings_dialog_input_field_start_margin" - android:visibility="visible"/> - - + android:visibility="gone"/> \ No newline at end of file diff --git a/WordPressEditor/src/main/res/values/strings.xml b/WordPressEditor/src/main/res/values/strings.xml index 0de2318cf639..88a5130a0669 100644 --- a/WordPressEditor/src/main/res/values/strings.xml +++ b/WordPressEditor/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Cancel Delete Save + Discard Can\'t insert media directly in HTML mode. Please switch back to visual mode. You are currently uploading media. Please wait until this completes. @@ -38,6 +39,9 @@ Horizontal alignment Caption (optional) + Discard unsaved changes? + Changes saved + Caption Alt text Link to diff --git a/WordPressEditor/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java b/WordPressEditor/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java index d3a9fa16a448..0a0b7860bc6e 100644 --- a/WordPressEditor/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java +++ b/WordPressEditor/src/test/java/org/wordpress/android/editor/EditorFragmentAbstractTest.java @@ -69,6 +69,11 @@ public void appendMediaFile(MediaFile mediaFile, String imageUrl, ImageLoader im public void appendGallery(MediaGallery mediaGallery) { } + @Override + public boolean hasFailedMediaUploads() { + return false; + } + @Override public void setTitlePlaceholder(CharSequence text) { diff --git a/example/build.gradle b/example/build.gradle index d7d728dba885..092a0951d237 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.1' + classpath 'com.android.tools.build:gradle:1.5.0' } } @@ -15,7 +15,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion 23 - buildToolsVersion "23.0.1" + buildToolsVersion "23.0.2" defaultConfig { applicationId "org.wordpress.editorexample" diff --git a/example/src/main/java/org/wordpress/example/EditorExampleActivity.java b/example/src/main/java/org/wordpress/example/EditorExampleActivity.java index 26b5e66c3e77..eb8e04f0866b 100644 --- a/example/src/main/java/org/wordpress/example/EditorExampleActivity.java +++ b/example/src/main/java/org/wordpress/example/EditorExampleActivity.java @@ -12,6 +12,7 @@ import org.wordpress.android.editor.EditorFragmentAbstract; import org.wordpress.android.editor.EditorFragmentAbstract.EditorFragmentListener; import org.wordpress.android.editor.EditorMediaUploadListener; +import org.wordpress.android.editor.ImageSettingsDialogFragment; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.helpers.MediaFile; @@ -31,6 +32,8 @@ public class EditorExampleActivity extends AppCompatActivity implements EditorFr public static final int ADD_MEDIA_ACTIVITY_REQUEST_CODE = 1111; public static final int ADD_MEDIA_FAIL_ACTIVITY_REQUEST_CODE = 1112; + public static final String MEDIA_REMOTE_ID_SAMPLE = "123"; + private static final int SELECT_PHOTO_MENU_POSITION = 0; private static final int SELECT_PHOTO_FAIL_MENU_POSITION = 1; @@ -60,6 +63,17 @@ public void onAttachFragment(Fragment fragment) { } } + @Override + public void onBackPressed() { + Fragment fragment = getFragmentManager() + .findFragmentByTag(ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG); + if (fragment != null && fragment.isVisible()) { + ((ImageSettingsDialogFragment) fragment).dismissFragment(); + } else { + super.onBackPressed(); + } + } + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { menu.add(0, SELECT_PHOTO_MENU_POSITION, 0, getString(R.string.select_photo)); @@ -139,7 +153,12 @@ public void onMediaRetryClicked(String mediaId) { } @Override - public void onMediaUploadCancelClicked(String mediaId) { + public void onMediaUploadCancelClicked(String mediaId, boolean delete) { + + } + + @Override + public void onFeaturedImageChanged(int mediaId) { } @@ -180,7 +199,8 @@ public void run() { count += 0.1; } - ((EditorMediaUploadListener) mEditorFragment).onMediaUploadSucceeded(mediaId, mediaUrl); + ((EditorMediaUploadListener) mEditorFragment).onMediaUploadSucceeded(mediaId, + MEDIA_REMOTE_ID_SAMPLE, mediaUrl); if (mFailedUploads.containsKey(mediaId)) { mFailedUploads.remove(mediaId); diff --git a/libs/editor-common/assets/ZSSRichTextEditor.js b/libs/editor-common/assets/ZSSRichTextEditor.js index 933292e05a26..d016bcc1e080 100755 --- a/libs/editor-common/assets/ZSSRichTextEditor.js +++ b/libs/editor-common/assets/ZSSRichTextEditor.js @@ -801,12 +801,12 @@ ZSSEditor.updateImage = function(url, alt) { }; -ZSSEditor.insertImage = function(url, alt) { +ZSSEditor.insertImage = function(url, remoteId, alt) { var space = '
'; var paragraphOpenTag = '<' + this.defaultParagraphSeparator + '>'; var paragraphCloseTag = ''; - var html = '' + alt + ''; + var html = '' + alt + ''; if (this.getFocusedField().getHTML().length == 0) { html = paragraphOpenTag + html; @@ -882,7 +882,7 @@ ZSSEditor.getImageContainerNodeWithIdentifier = function(imageNodeIdentifier) { * when replaceLocalImageWithRemoteImage() is called. * @param remoteImageUrl The URL of the remote image to display. */ -ZSSEditor.replaceLocalImageWithRemoteImage = function(imageNodeIdentifier, remoteImageUrl) { +ZSSEditor.replaceLocalImageWithRemoteImage = function(imageNodeIdentifier, remoteImageId, remoteImageUrl) { var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier); if (imageNode.length == 0) { @@ -895,6 +895,7 @@ ZSSEditor.replaceLocalImageWithRemoteImage = function(imageNodeIdentifier, remot image.onload = function () { imageNode.attr('src', image.src); + imageNode.addClass("wp-image-" + remoteImageId); ZSSEditor.markImageUploadDone(imageNodeIdentifier); var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments(); ZSSEditor.callback("callback-input", joinedArguments); @@ -906,6 +907,7 @@ ZSSEditor.replaceLocalImageWithRemoteImage = function(imageNodeIdentifier, remot // blogs are currently failing to download images due to access privilege issues. // imageNode.attr('src', image.src); + imageNode.addClass("wp-image-" + remoteImageId); ZSSEditor.markImageUploadDone(imageNodeIdentifier); var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments(); ZSSEditor.callback("callback-input", joinedArguments); @@ -956,7 +958,6 @@ ZSSEditor.markImageUploadDone = function(imageNodeIdentifier) { // remove uploading style imageNode.removeClass("uploading"); - imageNode.removeAttr("class"); // Remove all extra formatting nodes for progress if (imageNode.parent().attr("id") == this.getImageContainerIdentifier(imageNodeIdentifier)) { @@ -1038,6 +1039,46 @@ ZSSEditor.unmarkImageUploadFailed = function(imageNodeIdentifier, message) { } }; +/** + * @brief Marks all in-progress images as failed to upload + */ +ZSSEditor.markAllUploadingImagesAsFailed = function() { + var html = ZSSEditor.getField("zss_field_content").getHTML(); + var tmp = document.createElement( "div" ); + var tmpDom = $( tmp ).html( html ); + var matches = tmpDom.find("img.uploading"); + + for(var i = 0; i < matches.size(); i++) { + var mediaId = matches[i].getAttribute("data-wpid"); + if (mediaId != null) { + ZSSEditor.markImageUploadFailed(mediaId); + } + } +}; + +/** + * @brief Sends a callback with a list of failed images + */ +ZSSEditor.getFailedImages = function() { + var html = ZSSEditor.getField("zss_field_content").getHTML(); + var tmp = document.createElement( "div" ); + var tmpDom = $( tmp ).html( html ); + var matches = tmpDom.find("img.failed"); + + var functionArgument = "function=getFailedImages"; + var mediaIdArray = []; + + for (var i = 0; i < matches.size(); i ++) { + var mediaId = matches[i].getAttribute("data-wpid"); + if (mediaId != null) { + mediaIdArray.push(mediaId); + } + } + + var joinedArguments = functionArgument + defaultCallbackSeparator + "ids=" + mediaIdArray.toString(); + ZSSEditor.callback('callback-response-string', joinedArguments); +}; + /** * @brief Remove the image from the DOM. *