From c0c935fa4b3b6135be8099b637da57bede6b850f Mon Sep 17 00:00:00 2001 From: Alex Forcier Date: Wed, 27 Jan 2016 09:46:20 -1000 Subject: [PATCH 1/6] Squashed 'libs/editor/' changes from 6647346..d99856b d99856b Merge branch 'merge-wpandroid' into develop aee093e Implemented missing onAuthHeaderRequested method in the example app fdccfc1 Hide video fullscreen button 6fd8591 Pause video playback when the WebView is no longer visible to the user 84da174 Use a blank placeholder src instead of videopress.mp4 to trigger videopress video loading 435c012 Added some sanity checks for substring usage to JsCallbackReceiver 6c4d042 Fixed a bug where videos would sometimes get deleted if they were the only thing in the post d1aeaca Added support for video shortcodes on non-VideoPress sites 9f1b954 Merge branch 'feature/visual-editor' into feature/editor-video-playback a61fbf9 Added missing bracket in JS editor 040ceb8 Follow redirects when requesting thumbnails for the image settings dialog 4fbd821 Always defer to super.shouldInterceptRequest in editor WebView unless an auth header is present c933ee8 Merge branch 'feature/visual-editor' into feature/editor-video-playback 10a08fa Merge pull request #273 from wordpress-mobile/issue/15-track-editor-evnts bce6d92 Merge branch 'develop' into issue/15-track-editor-evnts 3e2c261 When looking up a VideoPress ID in the DB fails, refresh the blog media and check again c803d21 Remove the onError attribute from VideoPress videos if an empty video url is returned 4ca4c4c Added the ability to tap a placeholder VideoPress video to attempt to load it 1c638bf Don't use the placeholder poster image for videopress videos e9e8f7f Implemented missing method in example app d8a8510 fix typo in event name b13ac66 Track EDITOR_EDITED_IMAGE da3cd0c Add space after videos when converting from VideoPress shortcode ca6499f track most editor events 74b9932 use wp-analytics 1.1.0 1964844 Only apply custom headers for network URL resource requests b3dd7e3 Implement new EditorFragmentListener method in mock activity for integration tests fee3ffb Convert VideoPress shortcodes to video elements and back in the visual editor 9d56f49 Imported ZSSRichTextEditor video methods and callback methods from iOS 5b9a4c3 Imported video CSS from iOS 5bbc482 Added missing method implementation to test mock activity 77f7096 Retrieve an auth header if necessary when launching the image settings dialog 1214827 Use the safeToAddWordPressComAuthHeader utility method for resource requests in the new editor's WebView b2e2b74 Updated the utils artifact version for the editor 12f748a Moved setupUrlConnection() to the utils library 108a24c Renamed EditorFragment's setWebViewHeader to setCustomHttpHeader 1ac139e Use custom headers for image settings dialog image requests 542db23 Extracted some duplicated HttpURLConnection building code into a utility method 96984f0 When an Authorization HTTP header is detected, force using the HTTPS protocol in the WebViewClient 3302ab9 Added support for custom headers to the editor's WebView, set through the EditorFragmentAbstract 02ce3e4 Editor 0.5 version bump 190a822 Added full support for gallery types 25b3632 Added a fix enabling galleries to be added to the editor before the DOM loads acd7f9c Fixed creating a new post by making a gallery from the Media Library (the gallery wasn't being added to the new post) 1b6d47c Merge branch 'feature/visual-editor' into feature/editor-galleries b1cb369 Added support for creating galleries from local images, using a placeholder shortcode while uploading 92a3359 Extracted duplicate paragraph-wrapping code into a separate method in the ZSS editor 26d93c0 Added support for inserting galleries using media library images git-subtree-dir: libs/editor git-subtree-split: d99856bfb3e35e5c86f74af3f517739749e923ea --- WordPressEditor/build.gradle | 8 +- .../MockEditorActivity.java | 10 + .../android/editor/EditorFragment.java | 151 ++++- .../editor/EditorFragmentAbstract.java | 16 + .../editor/EditorMediaUploadListener.java | 1 + .../android/editor/EditorWebViewAbstract.java | 117 ++++ .../editor/ImageSettingsDialogFragment.java | 8 +- .../android/editor/JsCallbackReceiver.java | 14 +- .../android/editor/LegacyEditorFragment.java | 5 + .../OnJsEditorStateChangedListener.java | 1 + .../org/wordpress/android/editor/Utils.java | 35 +- .../editor/EditorFragmentAbstractTest.java | 5 + .../example/EditorExampleActivity.java | 10 + .../editor-common/assets/ZSSRichTextEditor.js | 546 +++++++++++++++++- libs/editor-common/assets/editor-android.css | 4 + libs/editor-common/assets/editor.css | 94 ++- libs/editor-common/assets/wpposter.svg | 7 + 17 files changed, 988 insertions(+), 44 deletions(-) create mode 100644 libs/editor-common/assets/wpposter.svg diff --git a/WordPressEditor/build.gradle b/WordPressEditor/build.gradle index 4dc82f05e1ee..df07667ac968 100644 --- a/WordPressEditor/build.gradle +++ b/WordPressEditor/build.gradle @@ -23,8 +23,8 @@ android { buildToolsVersion "23.0.2" defaultConfig { - versionCode 4 - versionName "0.4" + versionCode 5 + versionName "0.5" minSdkVersion 14 targetSdkVersion 23 } @@ -48,8 +48,8 @@ android { dependencies { 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' + compile 'org.wordpress:analytics:1.2.0' + compile 'org.wordpress:utils:1.7.0' // Test libraries testCompile 'junit:junit:4.11' 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 96e93382b662..fed7a083739d 100644 --- a/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java +++ b/WordPressEditor/src/androidTest/java/org.wordpress.android.editor/MockEditorActivity.java @@ -61,6 +61,16 @@ public void onFeaturedImageChanged(int mediaId) { } + @Override + public void onVideoPressInfoRequested(String videoId) { + + } + + @Override + public String onAuthHeaderRequested(String url) { + return ""; + } + @Override public void saveMediaFile(MediaFile mediaFile) { 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 22ec71085260..9366b73def16 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java @@ -30,16 +30,21 @@ import com.android.volley.toolbox.ImageLoader; +import org.json.JSONException; import org.json.JSONObject; +import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.analytics.AnalyticsTracker.Stat; 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.UrlUtils; import org.wordpress.android.util.helpers.MediaFile; import org.wordpress.android.util.helpers.MediaGallery; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; @@ -50,7 +55,8 @@ import java.util.concurrent.TimeUnit; public class EditorFragment extends EditorFragmentAbstract implements View.OnClickListener, View.OnTouchListener, - OnJsEditorStateChangedListener, OnImeBackListener, EditorMediaUploadListener { + OnJsEditorStateChangedListener, OnImeBackListener, EditorWebViewAbstract.AuthHeaderRequestListener, + EditorMediaUploadListener { private static final String ARG_PARAM_TITLE = "param_title"; private static final String ARG_PARAM_CONTENT = "param_content"; @@ -87,8 +93,10 @@ public class EditorFragment extends EditorFragmentAbstract implements View.OnCli private boolean mHideActionBarOnSoftKeyboardUp = false; private ConcurrentHashMap mWaitingMediaFiles; + private Set mWaitingGalleries; private Set mUploadingMediaIds; private Set mFailedMediaIds; + private MediaGallery mUploadingMediaGallery; private String mJavaScriptResult = ""; @@ -126,6 +134,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa } mWaitingMediaFiles = new ConcurrentHashMap<>(); + mWaitingGalleries = Collections.newSetFromMap(new ConcurrentHashMap()); mUploadingMediaIds = new HashSet<>(); mFailedMediaIds = new HashSet<>(); @@ -135,6 +144,13 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mWebView.setOnTouchListener(this); mWebView.setOnImeBackListener(this); + mWebView.setAuthHeaderRequestListener(this); + + if (mCustomHttpHeaders != null && mCustomHttpHeaders.size() > 0) { + for (Map.Entry 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() { @@ -222,6 +238,14 @@ public void onDetach() { 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()); @@ -365,6 +389,8 @@ protected void initJsEditor() { public void onClick(View v) { int id = v.getId(); if (id == R.id.format_bar_button_html) { + AnalyticsTracker.track(Stat.EDITOR_TAPPED_HTML); + // Don't switch to HTML mode if currently uploading media if (!mUploadingMediaIds.isEmpty()) { ((ToggleButton) v).setChecked(false); @@ -405,6 +431,7 @@ public void onClick(View v) { mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();"); } } else if (id == R.id.format_bar_button_media) { + AnalyticsTracker.track(Stat.EDITOR_TAPPED_IMAGE); ((ToggleButton) v).setChecked(false); if (mSourceView.getVisibility() == View.VISIBLE) { @@ -419,8 +446,10 @@ public void onClick(View v) { if (!((ToggleButton) v).isChecked()) { // The link button was checked when it was pressed; remove the current link mWebView.execJavaScriptFromString("ZSSEditor.unlink();"); + AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNLINK); return; } + AnalyticsTracker.track(Stat.EDITOR_TAPPED_LINK); ((ToggleButton) v).setChecked(false); @@ -479,6 +508,11 @@ public void onImeBack() { 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); @@ -706,11 +740,13 @@ public void run() { if (URLUtil.isNetworkUrl(mediaUrl)) { String mediaId = mediaFile.getMediaId(); mWebView.execJavaScriptFromString("ZSSEditor.insertImage('" + mediaUrl + "', '" + mediaId + "');"); + AnalyticsTracker.track(Stat.EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY); } else { String id = mediaFile.getMediaId(); mWebView.execJavaScriptFromString("ZSSEditor.insertLocalImage(" + id + ", '" + mediaUrl + "');"); mWebView.execJavaScriptFromString("ZSSEditor.setProgressOnImage(" + id + ", " + 0 + ");"); mUploadingMediaIds.add(id); + AnalyticsTracker.track(Stat.EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY); } } }); @@ -718,7 +754,35 @@ public void run() { @Override public void appendGallery(MediaGallery mediaGallery) { - // TODO + 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 + "', '" + + videoUrl + "', '" + posterUrl + "');"); + } + }); } @Override @@ -770,6 +834,7 @@ public void onMediaUploadFailed(final String mediaId) { mWebView.post(new Runnable() { @Override public void run() { + AnalyticsTracker.track(Stat.EDITOR_UPLOAD_MEDIA_FAILED); mWebView.execJavaScriptFromString("ZSSEditor.markImageUploadFailed(" + mediaId + ");"); mFailedMediaIds.add(mediaId); mUploadingMediaIds.remove(mediaId); @@ -777,6 +842,27 @@ public void run() { }); } + @Override + public void onGalleryMediaUploadSucceeded(final long galleryId, String remoteMediaId, int remaining) { + if (galleryId == mUploadingMediaGallery.getUniqueId()) { + ArrayList 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() { mWebView.post(new Runnable() { public void run() { @@ -820,6 +906,19 @@ public void run() { } 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();"); + + for (MediaGallery mediaGallery : mWaitingGalleries) { + appendGallery(mediaGallery); + } + + mWaitingGalleries.clear(); + } } }); } @@ -894,6 +993,7 @@ public void onClick(DialogInterface dialog, int id) { mWebView.post(new Runnable() { @Override public void run() { + AnalyticsTracker.track(Stat.EDITOR_UPLOAD_MEDIA_RETRIED); mWebView.execJavaScriptFromString("ZSSEditor.unmarkImageUploadFailed(" + mediaId + ");"); mWebView.execJavaScriptFromString("ZSSEditor.setProgressOnImage(" + mediaId + ", " + 0 + ");"); mFailedMediaIds.remove(mediaId); @@ -908,17 +1008,34 @@ public void run() { if (fragmentManager.findFragmentByTag(ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG) != null) { return; } - + AnalyticsTracker.track(Stat.EDITOR_EDITED_IMAGE); ImageSettingsDialogFragment imageSettingsDialogFragment = new ImageSettingsDialogFragment(); imageSettingsDialogFragment.setTargetFragment(this, ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_REQUEST_CODE); Bundle dialogBundle = new Bundle(); - dialogBundle.putString("imageMeta", meta.toString()); 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 headerMap = new HashMap<>(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)); @@ -933,6 +1050,8 @@ public void run() { ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG) .addToBackStack(null) .commit(); + + mWebView.notifyVisibilityChanged(false); break; } } @@ -950,6 +1069,11 @@ public void onLinkTapped(String url, String title) { linkDialogFragment.show(getFragmentManager(), "LinkDialogFragment"); } + @Override + public void onVideoPressInfoRequested(final String videoId) { + mEditorFragmentListener.onVideoPressInfoRequested(videoId); + } + public void onGetHtmlResponse(Map inputArgs) { String functionId = inputArgs.get("function"); @@ -1051,7 +1175,7 @@ private void clearFormatBarButtons() { private void onFormattingButtonClicked(ToggleButton toggleButton) { String tag = toggleButton.getTag().toString(); - + trackFormattingButtonClicked(toggleButton); if (mWebView.getVisibility() == View.VISIBLE) { mWebView.execJavaScriptFromString("ZSSEditor.set" + StringUtils.capitalize(tag) + "();"); } else { @@ -1059,6 +1183,23 @@ private void onFormattingButtonClicked(ToggleButton toggleButton) { } } + private void trackFormattingButtonClicked(ToggleButton toggleButton) { + int id = toggleButton.getId(); + if (id == R.id.format_bar_button_bold) { + AnalyticsTracker.track(Stat.EDITOR_TAPPED_BOLD); + } else if (id == R.id.format_bar_button_italic) { + AnalyticsTracker.track(Stat.EDITOR_TAPPED_ITALIC); + } else if (id == R.id.format_bar_button_ol) { + AnalyticsTracker.track(Stat.EDITOR_TAPPED_ORDERED_LIST); + } else if (id == R.id.format_bar_button_ul) { + AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNORDERED_LIST); + } else if (id == R.id.format_bar_button_quote) { + AnalyticsTracker.track(Stat.EDITOR_TAPPED_BLOCKQUOTE); + } else if (id == R.id.format_bar_button_strikethrough) { + AnalyticsTracker.track(Stat.EDITOR_TAPPED_STRIKETHROUGH); + } + } + /** * In HTML mode, applies formatting to selected text, or inserts formatting tag at current cursor position * @param toggleButton format bar button which was clicked 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 2e8b032c8370..ce874bd49a91 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java @@ -11,6 +11,9 @@ import org.wordpress.android.util.helpers.MediaFile; import org.wordpress.android.util.helpers.MediaGallery; +import java.util.HashMap; +import java.util.Map; + public abstract class EditorFragmentAbstract extends Fragment { public abstract void setTitle(CharSequence text); public abstract void setContent(CharSequence text); @@ -18,6 +21,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 void setUrlForVideoPressId(String videoPressId, String url, String posterUrl); public abstract boolean hasFailedMediaUploads(); public abstract void setTitlePlaceholder(CharSequence text); public abstract void setContentPlaceholder(CharSequence text); @@ -35,6 +39,8 @@ public abstract class EditorFragmentAbstract extends Fragment { protected ImageLoader mImageLoader; protected boolean mDebugModeEnabled; + protected HashMap mCustomHttpHeaders; + @Override public void onAttach(Activity activity) { super.onAttach(activity); @@ -83,6 +89,14 @@ public void setFeaturedImageId(int featuredImageId) { mFeaturedImageId = featuredImageId; } + public void setCustomHttpHeader(String name, String value) { + if (mCustomHttpHeaders == null) { + mCustomHttpHeaders = new HashMap<>(); + } + + mCustomHttpHeaders.put(name, value); + } + public void setDebugModeEnabled(boolean debugModeEnabled) { mDebugModeEnabled = debugModeEnabled; } @@ -113,6 +127,8 @@ public interface EditorFragmentListener { void onMediaRetryClicked(String mediaId); void onMediaUploadCancelClicked(String mediaId, boolean delete); void onFeaturedImageChanged(int mediaId); + void onVideoPressInfoRequested(String videoId); + String onAuthHeaderRequested(String url); // 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 6f052fac2b30..c8a25cd42783 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorMediaUploadListener.java @@ -4,4 +4,5 @@ public interface EditorMediaUploadListener { void onMediaUploadSucceeded(String localId, String remoteId, String remoteUrl); void onMediaUploadProgress(String localId, float progress); void onMediaUploadFailed(String localId); + void onGalleryMediaUploadSucceeded(long galleryId, String remoteId, int remaining); } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java index 979e167fcf3e..5aec2abd4547 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorWebViewAbstract.java @@ -1,18 +1,32 @@ package org.wordpress.android.editor; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.Context; +import android.os.Build; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.KeyEvent; +import android.view.View; import android.webkit.ConsoleMessage; import android.webkit.JsResult; +import android.webkit.URLUtil; import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.HTTPUtils; +import org.wordpress.android.util.StringUtils; +import org.wordpress.android.util.UrlUtils; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.Map; /** * A text editor WebView with support for JavaScript execution. @@ -21,6 +35,9 @@ public abstract class EditorWebViewAbstract extends WebView { public abstract void execJavaScriptFromString(String javaScript); private OnImeBackListener mOnImeBackListener; + private AuthHeaderRequestListener mAuthHeaderRequestListener; + + private Map mHeaderMap = new HashMap<>(); public EditorWebViewAbstract(Context context, AttributeSet attrs) { super(context, attrs); @@ -43,6 +60,73 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { AppLog.e(AppLog.T.EDITOR, description); } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + String url = request.getUrl().toString(); + + if (!URLUtil.isNetworkUrl(url)) { + return super.shouldInterceptRequest(view, request); + } + + // Request and add an authorization header for HTTPS resource requests. + // Use https:// when requesting the auth header, in case the resource is incorrectly using http://. + // If an auth header is returned, force https:// for the actual HTTP request. + String authHeader = mAuthHeaderRequestListener.onAuthHeaderRequested(UrlUtils.makeHttps(url)); + if (StringUtils.notNullStr(authHeader).length() > 0) { + try { + url = UrlUtils.makeHttps(url); + + // Keep any existing request headers from the WebResourceRequest + Map headerMap = request.getRequestHeaders(); + for (Map.Entry entry : mHeaderMap.entrySet()) { + headerMap.put(entry.getKey(), entry.getValue()); + } + headerMap.put("Authorization", authHeader); + + HttpURLConnection conn = HTTPUtils.setupUrlConnection(url, headerMap); + return new WebResourceResponse(conn.getContentType(), conn.getContentEncoding(), + conn.getInputStream()); + } catch (IOException e) { + AppLog.e(AppLog.T.EDITOR, e); + } + } + + return super.shouldInterceptRequest(view, request); + } + + /** + * Compatibility method for API < 21 + */ + @SuppressWarnings("deprecation") + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + if (!URLUtil.isNetworkUrl(url)) { + return super.shouldInterceptRequest(view, url); + } + + // Request and add an authorization header for HTTPS resource requests. + // Use https:// when requesting the auth header, in case the resource is incorrectly using http://. + // If an auth header is returned, force https:// for the actual HTTP request. + String authHeader = mAuthHeaderRequestListener.onAuthHeaderRequested(UrlUtils.makeHttps(url)); + if (StringUtils.notNullStr(authHeader).length() > 0) { + try { + url = UrlUtils.makeHttps(url); + + Map headerMap = new HashMap<>(mHeaderMap); + headerMap.put("Authorization", authHeader); + + HttpURLConnection conn = HTTPUtils.setupUrlConnection(url, headerMap); + return new WebResourceResponse(conn.getContentType(), conn.getContentEncoding(), + conn.getInputStream()); + } catch (IOException e) { + AppLog.e(AppLog.T.EDITOR, e); + } + } + + return super.shouldInterceptRequest(view, url); + } }); this.setWebChromeClient(new WebChromeClient() { @@ -65,10 +149,35 @@ public boolean onCheckIsTextEditor() { return true; } + @Override + public void setVisibility(int visibility) { + notifyVisibilityChanged(visibility == View.VISIBLE); + super.setVisibility(visibility); + } + + /** + * Handles events that should be triggered when the WebView is hidden or is shown to the user + * @param visible the new visibility status of the WebView + */ + public void notifyVisibilityChanged(boolean visible) { + if (!visible) { + this.post(new Runnable() { + @Override + public void run() { + execJavaScriptFromString("ZSSEditor.pauseAllVideos();"); + } + }); + } + } + public void setOnImeBackListener(OnImeBackListener listener) { mOnImeBackListener = listener; } + public void setAuthHeaderRequestListener(AuthHeaderRequestListener listener) { + mAuthHeaderRequestListener = listener; + } + @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { @@ -78,4 +187,12 @@ public boolean onKeyPreIme(int keyCode, KeyEvent event) { } return super.onKeyPreIme(keyCode, event); } + + public void setCustomHeader(String name, String value) { + mHeaderMap.put(name, value); + } + + public interface AuthHeaderRequestListener { + String onAuthHeaderRequested(String url); + } } 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 ff6c0a99c84b..ab2932d662ab 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/ImageSettingsDialogFragment.java @@ -32,6 +32,7 @@ import java.util.Arrays; import java.util.Locale; +import java.util.Map; /** * A full-screen DialogFragment with image settings. @@ -59,6 +60,8 @@ public class ImageSettingsDialogFragment extends DialogFragment { private boolean mIsFeatured; + private Map mHttpHeaders; + private CharSequence mPreviousActionBarTitle; private boolean mPreviousHomeAsUpEnabled; private View mPreviousCustomView; @@ -145,6 +148,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa try { mImageMeta = new JSONObject(bundle.getString("imageMeta")); + mHttpHeaders = (Map) bundle.getSerializable("headerMap"); + final String imageSrc = mImageMeta.getString("src"); final String imageFilename = imageSrc.substring(imageSrc.lastIndexOf("/") + 1); @@ -175,7 +180,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mIsFeatured = bundle.getBoolean("isFeatured", false); mFeaturedCheckBox.setChecked(mIsFeatured); } - } catch (JSONException e1) { AppLog.d(AppLog.T.EDITOR, "Missing JSON properties"); } @@ -318,7 +322,7 @@ private void loadThumbnail(final String src, final ImageView thumbnailImage) { @Override public void run() { if (isAdded()) { - final Uri localUri = Utils.downloadExternalMedia(getActivity(), Uri.parse(src)); + final Uri localUri = Utils.downloadExternalMedia(getActivity(), Uri.parse(src), mHttpHeaders); if (getActivity() != null) { getActivity().runOnUiThread(new Runnable() { 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 5a56ff530e27..6e74071c66a4 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/JsCallbackReceiver.java @@ -30,6 +30,8 @@ public class JsCallbackReceiver { private static final String CALLBACK_IMAGE_TAP = "callback-image-tap"; private static final String CALLBACK_LINK_TAP = "callback-link-tap"; + private static final String CALLBACK_VIDEOPRESS_INFO_REQUEST = "callback-videopress-info-request"; + private static final String CALLBACK_LOG = "callback-log"; private static final String CALLBACK_RESPONSE_STRING = "callback-response-string"; @@ -159,14 +161,22 @@ public void executeCallback(String callbackId, String params) { mListener.onLinkTapped(url, title); break; + case CALLBACK_VIDEOPRESS_INFO_REQUEST: + // Extract the VideoPress id from the callback string (stripping the 'id=' part of the callback string) + if (params.length() > 3) { + mListener.onVideoPressInfoRequested(params.substring(3)); + } + break; case CALLBACK_LOG: // Strip 'msg=' from beginning of string - AppLog.d(AppLog.T.EDITOR, callbackId + ": " + params.substring(4)); + if (params.length() > 4) { + AppLog.d(AppLog.T.EDITOR, callbackId + ": " + params.substring(4)); + } break; case CALLBACK_RESPONSE_STRING: AppLog.d(AppLog.T.EDITOR, callbackId + ": " + params); Set responseDataSet; - if (params.startsWith("function=")) { + if (params.startsWith("function=") && params.contains(JS_CALLBACK_DELIMITER)) { String functionName = params.substring("function=".length(), params.indexOf(JS_CALLBACK_DELIMITER)); List responseIds = new ArrayList<>(); 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 eb3747dfcf3e..3ca172e685a6 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 void setUrlForVideoPressId(String videoPressId, String url, String posterUrl) { + + } + @Override public boolean hasFailedMediaUploads() { return false; 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 89941ba79ead..27d9b7fa5082 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/OnJsEditorStateChangedListener.java @@ -10,5 +10,6 @@ public interface OnJsEditorStateChangedListener { void onSelectionStyleChanged(Map changeSet); void onMediaTapped(String mediaId, String url, JSONObject meta, String uploadStatus); void onLinkTapped(String url, String title); + void onVideoPressInfoRequested(String videoId); void onGetHtmlResponse(Map responseArgs); } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java index cf06eb3e1a71..c4c5665defef 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/Utils.java @@ -6,19 +6,19 @@ import android.net.Uri; import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.HTTPUtils; import java.io.BufferedReader; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URL; +import java.net.HttpURLConnection; import java.net.URLDecoder; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -27,7 +27,6 @@ import java.util.StringTokenizer; public class Utils { - public static String getHtmlFromFile(Activity activity, String filename) { try { AssetManager assetManager = activity.getAssets(); @@ -156,24 +155,40 @@ public static Map getChangeMapFromSets(Set oldSet, Set new return changeMap; } - public static Uri downloadExternalMedia(Context context, Uri imageUri) { + public static Uri downloadExternalMedia(Context context, Uri imageUri, Map headers) { if(context != null && imageUri != null) { File cacheDir = null; - if(context.getApplicationContext() != null) { + if (context.getApplicationContext() != null) { cacheDir = context.getCacheDir(); } try { InputStream inputStream; - if(imageUri.toString().startsWith("content://")) { + if (imageUri.toString().startsWith("content://")) { inputStream = context.getContentResolver().openInputStream(imageUri); - if(inputStream == null) { + if (inputStream == null) { AppLog.e(AppLog.T.UTILS, "openInputStream returned null"); return null; } } else { - inputStream = (new URL(imageUri.toString())).openStream(); + if (headers == null) { + headers = Collections.emptyMap(); + } + + HttpURLConnection conn = HTTPUtils.setupUrlConnection(imageUri.toString(), headers); + + // If the HTTP response is a redirect, follow it + int responseCode = conn.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + if (responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP + || responseCode == HttpURLConnection.HTTP_SEE_OTHER) { + conn = HTTPUtils.setupUrlConnection(conn.getHeaderField("Location"), headers); + } + } + + inputStream = conn.getInputStream(); } String fileName = "thumb-" + System.currentTimeMillis(); @@ -183,7 +198,7 @@ public static Uri downloadExternalMedia(Context context, Uri imageUri) { byte[] data = new byte[1024]; int count; - while((count = inputStream.read(data)) != -1) { + while ((count = inputStream.read(data)) != -1) { output.write(data, 0, count); } 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 0a0b7860bc6e..bf2427eaa21a 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 void setUrlForVideoPressId(String videoPressId, String url, String posterUrl) { + + } + @Override public boolean hasFailedMediaUploads() { return false; diff --git a/example/src/main/java/org/wordpress/example/EditorExampleActivity.java b/example/src/main/java/org/wordpress/example/EditorExampleActivity.java index eb8e04f0866b..0edcce21abae 100644 --- a/example/src/main/java/org/wordpress/example/EditorExampleActivity.java +++ b/example/src/main/java/org/wordpress/example/EditorExampleActivity.java @@ -162,6 +162,16 @@ public void onFeaturedImageChanged(int mediaId) { } + @Override + public void onVideoPressInfoRequested(String videoId) { + + } + + @Override + public String onAuthHeaderRequested(String url) { + return ""; + } + @Override public void onEditorFragmentInitialized() { // arbitrary setup diff --git a/libs/editor-common/assets/ZSSRichTextEditor.js b/libs/editor-common/assets/ZSSRichTextEditor.js index d016bcc1e080..511240a39aa2 100755 --- a/libs/editor-common/assets/ZSSRichTextEditor.js +++ b/libs/editor-common/assets/ZSSRichTextEditor.js @@ -47,6 +47,9 @@ ZSSEditor.currentSelection; // The current editing image ZSSEditor.currentEditingImage; +// The current editing video +ZSSEditor.currentEditingVideo; + // The current editing link ZSSEditor.currentEditingLink; @@ -62,6 +65,10 @@ ZSSEditor.lastTappedNode = null; // The default paragraph separator ZSSEditor.defaultParagraphSeparator = 'p'; +// Video format tags supported by the [video] shortcode: https://codex.wordpress.org/Video_Shortcode +// mp4, m4v and webm prioritized since they're supported by the stock player as of Android API 23 +ZSSEditor.videoShortcodeFormats = ["mp4", "m4v", "webm", "ogv", "wmv", "flv"]; + /** * The initializer function that must be called onLoad */ @@ -628,6 +635,25 @@ ZSSEditor.setBackgroundColor = function(color) { ZSSEditor.sendEnabledStyles(); }; +/** + * @brief Wraps given HTML in paragraph tags and appends a new line + * @details This method makes sure that passed HTML is wrapped in a separate paragraph. + * It also appends a new opening paragraph tag and a space. This step is necessary to keep any spans or + * divs in the HTML from being read by the WebView as a style and applied to all future paragraphs. + */ +ZSSEditor.wrapInParagraphTags = function(html) { + var space = '
'; + var paragraphOpenTag = '<' + this.defaultParagraphSeparator + '>'; + var paragraphCloseTag = ''; + + if (this.getFocusedField().getHTML().length == 0) { + html = paragraphOpenTag + html; + } + html = html + paragraphCloseTag + paragraphOpenTag + space; + + return html; +}; + // Needs addClass method ZSSEditor.insertLink = function(url, title) { @@ -802,18 +828,9 @@ ZSSEditor.updateImage = function(url, alt) { }; ZSSEditor.insertImage = function(url, remoteId, alt) { - var space = '
'; - var paragraphOpenTag = '<' + this.defaultParagraphSeparator + '>'; - var paragraphCloseTag = ''; - var html = '' + alt + ''; - if (this.getFocusedField().getHTML().length == 0) { - html = paragraphOpenTag + html; - } - html = html + paragraphCloseTag + paragraphOpenTag + space; - - this.insertHTML(html); + this.insertHTML(this.wrapInParagraphTags(html)); this.sendEnabledStyles(); }; @@ -831,9 +848,6 @@ ZSSEditor.insertImage = function(url, remoteId, alt) { * does not check for that. It would be a mistake. */ ZSSEditor.insertLocalImage = function(imageNodeIdentifier, localImageUrl) { - var space = '
'; - var paragraphOpenTag = '<' + this.defaultParagraphSeparator + '>'; - var paragraphCloseTag = ''; var progressIdentifier = this.getImageProgressIdentifier(imageNodeIdentifier); var imageContainerIdentifier = this.getImageContainerIdentifier(imageNodeIdentifier); var imgContainerStart = ''; @@ -842,12 +856,7 @@ ZSSEditor.insertLocalImage = function(imageNodeIdentifier, localImageUrl) { var image = ''; var html = imgContainerStart + progress+image + imgContainerEnd; - if (this.getFocusedField().getHTML().length == 0) { - html = paragraphOpenTag + html; - } - html = html + paragraphCloseTag + paragraphOpenTag + space; - - this.insertHTML(html); + this.insertHTML(this.wrapInParagraphTags(html)); this.sendEnabledStyles(); }; @@ -1097,6 +1106,417 @@ ZSSEditor.removeImage = function(imageNodeIdentifier) { } }; +/** + * @brief Inserts a video tag using the videoURL as source and posterURL as the + * image to show while video is loading. + * + * @param videoURL the url of the video to present + * @param posterURL the url of an image to show while the video is loading + * @param alt the alt description when the video is not supported. + * + */ +ZSSEditor.insertVideo = function(videoURL, posterURL, alt) { + var html = ''; + + this.insertHTML(html); + this.sendEnabledStyles(); +}; + +/** + * @brief Inserts a video tag marked with a identifier using only a poster image. Useful for videos that need to be uploaded. + * @details By inserting a video with only a porter URL, we can make sure the video element is shown to the user + * as soon as it's selected for uploading. Once the video is successfully uploaded + * the application should call replaceLocalVideoWithRemoteVideo(). + * + * @param videoNodeIdentifier This is a unique ID provided by the caller. It exists as + * a mechanism to update the video node with the remote URL + * when replaceLocalVideoWithRemoteVideo() is called. + * @param posterURL The URL of a poster image to display while the video is being uploaded. + */ +ZSSEditor.insertInProgressVideoWithIDUsingPosterImage = function(videoNodeIdentifier, posterURL) { + var space = ' '; + var progressIdentifier = this.getVideoProgressIdentifier(videoNodeIdentifier); + var videoContainerIdentifier = this.getVideoContainerIdentifier(videoNodeIdentifier); + var videoContainerStart = ''; + var videoContainerEnd = ''; + var progress = ''; + var video = ''; + var html = space + videoContainerStart + progress + video + videoContainerEnd + space; + this.insertHTML(html); + this.sendEnabledStyles(); +}; + +ZSSEditor.getVideoNodeWithIdentifier = function(videoNodeIdentifier) { + return $('video[data-wpid="' + videoNodeIdentifier+'"]'); +}; + +ZSSEditor.getVideoProgressIdentifier = function(videoNodeIdentifier) { + return 'progress_' + videoNodeIdentifier; +}; + +ZSSEditor.getVideoProgressNodeWithIdentifier = function(videoNodeIdentifier) { + return $('#'+this.getVideoProgressIdentifier(videoNodeIdentifier)); +}; + +ZSSEditor.getVideoContainerIdentifier = function(videoNodeIdentifier) { + return 'video_container_' + videoNodeIdentifier; +}; + +ZSSEditor.getVideoContainerNodeWithIdentifier = function(videoNodeIdentifier) { + return $('#'+this.getVideoContainerIdentifier(videoNodeIdentifier)); +}; + + +/** + * @brief Replaces a local Video URL with a remote Video URL. Useful for videos that have + * just finished uploading. + * @details The remote Video can be available after a while, when uploading Videos. This method + * allows for the remote URL to be loaded once the upload completes. + * + * @param videoNodeIdentifier This is a unique ID provided by the caller. It exists as + * a mechanism to update the Video node with the remote URL + * when replaceLocalVideoWithRemoteVideo() is called. + * @param remoteVideoUrl The URL of the remote Video to display. + * @param remotePosterUrl The URL of thre remote poster image to display + * @param videopressID VideoPress Guid of the video if any + */ +ZSSEditor.replaceLocalVideoWithRemoteVideo = function(videoNodeIdentifier, remoteVideoUrl, remotePosterUrl, videopressID) { + var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); + + if (videoNode.length == 0) { + // even if the Video is not present anymore we must do callback + this.markVideoUploadDone(videoNodeIdentifier); + return; + } + videoNode.attr('src', remoteVideoUrl); + videoNode.attr('controls', ''); + videoNode.attr('preload', 'metadata'); + if (videopressID != '') { + videoNode.attr('data-wpvideopress', videopressID); + } + videoNode.attr('poster', remotePosterUrl); + var thisObj = this; + videoNode.on('webkitbeginfullscreen', function (event){ thisObj.sendVideoFullScreenStarted(); } ); + videoNode.on('webkitendfullscreen', function (event){ thisObj.sendVideoFullScreenEnded(); } ); + videoNode.on('error', function(event) { videoNode.load()} ); + this.markVideoUploadDone(videoNodeIdentifier); +}; + +/** + * @brief Update the progress indicator for the Video identified with the value in progress. + * + * @param VideoNodeIdentifier This is a unique ID provided by the caller. + * @param progress A value between 0 and 1 indicating the progress on the Video. + */ +ZSSEditor.setProgressOnVideo = function(videoNodeIdentifier, progress) { + var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); + if (videoNode.length == 0){ + return; + } + if (progress < 1){ + videoNode.addClass("uploading"); + } + + var videoProgressNode = this.getVideoProgressNodeWithIdentifier(videoNodeIdentifier); + if (videoProgressNode.length == 0){ + return; + } + videoProgressNode.attr("value",progress); +}; + +/** + * @brief Notifies that the Video upload as finished + * + * @param VideoNodeIdentifier The unique Video ID for the uploaded Video + */ +ZSSEditor.markVideoUploadDone = function(videoNodeIdentifier) { + var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); + if (videoNode.length > 0) { + + // remove identifier attributed from Video + videoNode.removeAttr('data-wpid'); + + // remove uploading style + videoNode.removeClass("uploading"); + videoNode.removeAttr("class"); + + // Remove all extra formatting nodes for progress + if (videoNode.parent().attr("id") == this.getVideoContainerIdentifier(videoNodeIdentifier)) { + // remove id from container to avoid to report a user removal + videoNode.parent().attr("id", ""); + videoNode.parent().replaceWith(videoNode); + } + } + var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments(); + ZSSEditor.callback("callback-input", joinedArguments); + // We invoke the sendVideoReplacedCallback with a delay to avoid for + // it to be ignored by the webview because of the previous callback being done. + var thisObj = this; + setTimeout(function() { thisObj.sendVideoReplacedCallback(videoNodeIdentifier);}, 500); +}; + +/** + * @brief Callbacks to native that the video upload as finished and the local url was replaced by the remote url + * + * @param videoNodeIdentifier the unique video ID for the uploaded Video + */ +ZSSEditor.sendVideoReplacedCallback = function( videoNodeIdentifier ) { + var arguments = ['id=' + encodeURIComponent( videoNodeIdentifier )]; + + var joinedArguments = arguments.join( defaultCallbackSeparator ); + + this.callback("callback-video-replaced", joinedArguments); +}; + +/** + * @brief Callbacks to native that the video entered full screen mode + * + */ +ZSSEditor.sendVideoFullScreenStarted = function() { + this.callback("callback-video-fullscreen-started", "empty"); +}; + +/** + * @brief Callbacks to native that the video entered full screen mode + * + */ +ZSSEditor.sendVideoFullScreenEnded = function() { + this.callback("callback-video-fullscreen-ended", "empty"); +}; + +/** + * @brief Callbacks to native that the video upload as finished and the local url was replaced by the remote url + * + * @param videoNodeIdentifier the unique video ID for the uploaded Video + */ +ZSSEditor.sendVideoPressInfoRequest = function( videoPressID ) { + var arguments = ['id=' + encodeURIComponent( videoPressID )]; + + var joinedArguments = arguments.join( defaultCallbackSeparator ); + + this.callback("callback-videopress-info-request", joinedArguments); +}; + + +/** + * @brief Marks the Video as failed to upload + * + * @param VideoNodeIdentifier This is a unique ID provided by the caller. + * @param message A message to show to the user, overlayed on the Video + */ +ZSSEditor.markVideoUploadFailed = function(videoNodeIdentifier, message) { + var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); + if (videoNode.length == 0){ + return; + } + + var sizeClass = ''; + if ( videoNode[0].width > 480 && videoNode[0].height > 240 ) { + sizeClass = "largeFail"; + } else if ( videoNode[0].width < 100 || videoNode[0].height < 100 ) { + sizeClass = "smallFail"; + } + + videoNode.addClass('failed'); + + var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier); + if(videoContainerNode.length != 0){ + videoContainerNode.attr("data-failed", message); + videoNode.removeClass("uploading"); + videoContainerNode.addClass('failed'); + videoContainerNode.addClass(sizeClass); + } + + var videoProgressNode = this.getVideoProgressNodeWithIdentifier(videoNodeIdentifier); + if (videoProgressNode.length != 0){ + videoProgressNode.addClass('failed'); + } +}; + +/** + * @brief Unmarks the Video as failed to upload + * + * @param VideoNodeIdentifier This is a unique ID provided by the caller. + */ +ZSSEditor.unmarkVideoUploadFailed = function(videoNodeIdentifier, message) { + var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); + if (videoNode.length != 0){ + videoNode.removeClass('failed'); + } + + var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier); + if(videoContainerNode.length != 0){ + videoContainerNode.removeAttr("data-failed"); + videoContainerNode.removeClass('failed'); + } + + var videoProgressNode = this.getVideoProgressNodeWithIdentifier(videoNodeIdentifier); + if (videoProgressNode.length != 0){ + videoProgressNode.removeClass('failed'); + } +}; + +/** + * @brief Remove the Video from the DOM. + * + * @param videoNodeIdentifier This is a unique ID provided by the caller. + */ +ZSSEditor.removeVideo = function(videoNodeIdentifier) { + var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); + if (videoNode.length != 0){ + videoNode.remove(); + } + + // if Video is inside options container we need to remove the container + var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier); + if (videoContainerNode.length != 0){ + //reset id before removal to avoid detection of user removal + videoContainerNode.attr("id",""); + videoContainerNode.remove(); + } +}; + +ZSSEditor.replaceVideoPressVideosForShortcode = function ( html) { + // call methods to restore any transformed content from its visual presentation to its source code. + var regex = /]*data-wpvideopress="([\s\S]+?)"[^>]*>*<\/video>/g; + var str = html.replace( regex, ZSSEditor.removeVideoPressVisualFormattingCallback ); + + return str; +} + +ZSSEditor.replaceVideosForShortcode = function ( html) { + var regex = /<\/video>/g; + var str = html.replace( regex, ZSSEditor.removeVideoVisualFormattingCallback ); + + return str; +} + +ZSSEditor.removeVideoPressVisualFormattingCallback = function( match, content ) { + return "[wpvideo " + content + "]"; +} + +ZSSEditor.removeVideoVisualFormattingCallback = function( match, content ) { + var videoElement = $.parseHTML(match)[0]; + + // Remove editor playback attributes + videoElement.removeAttribute("onclick"); + videoElement.removeAttribute("controls"); + videoElement.removeAttribute("webkit-playsinline"); + if (videoElement.getAttribute("preload") == "metadata") { + // The "metadata" setting is the WP default and is usually automatically stripped from the shortcode. + // If it's present, it was probably set by this editor and we should remove it. Even if it wasn't, removing it + // won't affect anything as it's the default setting for the preload attribute. + videoElement.removeAttribute("preload"); + } + + // If filetype attributes exist, the src attribute wasn't there originally and we should remove it + for (var i = 0; i < ZSSEditor.videoShortcodeFormats.length; i++) { + var format = ZSSEditor.videoShortcodeFormats[i]; + if (videoElement.hasAttribute(format)) { + videoElement.removeAttribute("src"); + break; + } + } + + var shortcode = videoElement.outerHTML.replace(//g, "]"); + return shortcode; +} + +ZSSEditor.applyVideoPressFormattingCallback = function( match ) { + if (match.attrs.numeric.length == 0) { + return match.content; + } + var videopressID = match.attrs.numeric[0]; + var posterSVG = '"wpposter.svg"'; + // The empty 'onclick' is important. It prevents the cursor jumping to the end + // of the content body when `-webkit-user-select: none` is set and the video is tapped. + var out = ''; + + out = out + '
'; + return out; +} + +ZSSEditor.applyVideoFormattingCallback = function( match ) { + // Find the tag containing the video source + var srcTag = ""; + + if (match.attrs.named['src']) { + srcTag = "src"; + } else { + for (var i = 0; i < ZSSEditor.videoShortcodeFormats.length; i++) { + var format = ZSSEditor.videoShortcodeFormats[i]; + if (match.attrs.named[format]) { + srcTag = format; + break; + } + } + } + + if (srcTag.length == 0) { + return match.content; + } + + var out = '
'; + + this.insertHTML(this.wrapInParagraphTags(container)); +} + +ZSSEditor.replacePlaceholderGallery = function( placeholderId, imageIds, type, columns ) { + var span = 'span#' + placeholderId + '.gallery_container'; + + var shortcode; + if (type) { + shortcode = '[gallery type="' + type + '" ids="' + imageIds + '"]'; + } else { + shortcode = '[gallery columns="' + columns + '" ids="' + imageIds + '"]'; + } + + $(span).replaceWith(shortcode); +} + // MARK: - Commands /** @@ -1569,6 +2020,8 @@ ZSSEditor.removeCaptionFormattingCallback = function( match, content ) { */ ZSSEditor.applyVisualFormatting = function( html ) { var str = wp.shortcode.replace( 'caption', html, ZSSEditor.applyCaptionFormatting ); + str = wp.shortcode.replace( 'wpvideo', str, ZSSEditor.applyVideoPressFormattingCallback ); + str = wp.shortcode.replace( 'video', str, ZSSEditor.applyVideoFormattingCallback ); return str; } @@ -1584,6 +2037,8 @@ ZSSEditor.removeVisualFormatting = function( html ) { var str = html; str = ZSSEditor.removeImageSelectionFormattingFromHTML( str ); str = ZSSEditor.removeCaptionFormatting( str ); + str = ZSSEditor.replaceVideoPressVideosForShortcode( str ); + str = ZSSEditor.replaceVideosForShortcode( str ); return str; } @@ -2196,10 +2651,11 @@ ZSSField.prototype.emptyFieldIfNoContents = function() { if (text.length == 0 || text == '\u000A') { var hasChildImages = (this.wrappedObject.find('img').length > 0); + var hasChildVideos = (this.wrappedObject.find('video').length > 0); var hasUnorderedList = (this.wrappedObject.find('ul').length > 0); var hasOrderedList = (this.wrappedObject.find('ol').length > 0); - if (!hasChildImages && !hasUnorderedList && !hasOrderedList) { + if (!hasChildImages && !hasChildVideos && !hasUnorderedList && !hasOrderedList) { this.wrappedObject.empty(); } } @@ -2339,6 +2795,27 @@ ZSSField.prototype.handleTapEvent = function(e) { ZSSEditor.removeImageSelectionFormatting( ZSSEditor.currentEditingImage ); ZSSEditor.currentEditingImage = null; } + + if (targetNode.nodeName.toLowerCase() == 'video') { + // If the video is uploading, or is a local image do not select it. + if (targetNode.dataset.wpvideopress) { + if (targetNode.src.length == 0 || targetNode.src == 'file:///android_asset/') { + // If the tapped video is a placeholder for a VideoPress video, send out an update request. + // This provides a way to load the video for Android API<19, where the onError property function in + // the placeholder video isn't being triggered, and sendVideoPressInfoRequest is never called. + // This is also used to manually retry loading a VideoPress video after the onError attribute has + // been stripped for the video tag. + targetNode.setAttribute("onerror", ""); + ZSSEditor.sendVideoPressInfoRequest(targetNode.dataset.wpvideopress); + return; + } + } + + if (targetNode.dataset.wpid) { + this.sendVideoTappedCallback( targetNode ); + return; + } + } } }; @@ -2361,6 +2838,35 @@ ZSSField.prototype.sendImageTappedCallback = function( imageNode ) { setTimeout(function() { thisObj.callback('callback-image-tap', joinedArguments);}, 500); } +ZSSField.prototype.sendVideoTappedCallback = function( videoNode ) { + var videoId = ""; + if ( videoNode.hasAttribute( 'data-wpid' ) ){ + videoId = videoNode.getAttribute( 'data-wpid' ) + } + var arguments = ['id=' + encodeURIComponent( videoId ), + 'url=' + encodeURIComponent( videoNode.src )]; + + var joinedArguments = arguments.join( defaultCallbackSeparator ); + + ZSSEditor.callback('callback-video-tap', joinedArguments); +} + +/** + * @brief Callbacks to native that the video entered full screen mode + * + */ +ZSSField.prototype.sendVideoFullScreenStarted = function() { + this.callback("callback-video-fullscreen-started", "empty"); +}; + +/** + * @brief Callbacks to native that the video entered full screen mode + * + */ +ZSSField.prototype.sendVideoFullScreenEnded = function() { + this.callback("callback-video-fullscreen-ended", "empty"); +}; + // MARK: - Callback Execution ZSSField.prototype.callback = function(callbackScheme, callbackPath) { diff --git a/libs/editor-common/assets/editor-android.css b/libs/editor-common/assets/editor-android.css index 9ff9135a6d73..ceed82213289 100644 --- a/libs/editor-common/assets/editor-android.css +++ b/libs/editor-common/assets/editor-android.css @@ -12,6 +12,10 @@ } } +video::-webkit-media-controls-fullscreen-button { + display: none; +} + /* Used only on older APIs (API<19), which don't support CSS filter effects (specifically, blur). */ .edit-container .edit-overlay-bg { position: absolute; diff --git a/libs/editor-common/assets/editor.css b/libs/editor-common/assets/editor.css index 84f78191ee9b..6279603b30d4 100644 --- a/libs/editor-common/assets/editor.css +++ b/libs/editor-common/assets/editor.css @@ -106,6 +106,17 @@ img { opacity:1; } +video { + width: auto; + height: auto; + margin: 0px 0px 0px 0px; + min-width: 30px; + min-height: 30px; + max-width: 100%; + opacity:1; + background:#2e4453 +} + a { color: #0087be; text-decoration: none; @@ -196,7 +207,81 @@ span.img_container.failed.smallFail::after { content:attr(data-failed); } -/* Image editing overlay styles */ +video.uploading { + opacity:0.3; + -webkit-user-select: none; +} + +video.failed { + -webkit-filter: blur(4px) grayscale(0.3); + margin:-1px; + padding:1px; + z-index:-1; + -webkit-user-select: none; + overflow: hidden; +} + +span.video_container { + position: relative; + display: inline-block; + -webkit-user-select: none; +} + +span.video_container.failed { + overflow: hidden; +} + +span.video_container.failed::before { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + left:0%; + text-align: center; + color: white; + width:100%; + height:85px; + opacity:1; + z-index:10; + -webkit-user-select: none; + pointer-events: none; + content:""; + background:url('data:image/svg+xml;utf8,') no-repeat center top; +} + +span.video_container.failed.largeFail::before { + height:170px; + background:url('data:image/svg+xml;utf8,') no-repeat center top; +} + +span.video_container.failed::after { + position: absolute; + padding:27px 0 0 0; + top:50%; + left:0%; + font-family:OpenSans; + font-size:20px; + font-weight:600; + text-align: center; + text-shadow: 0 1px 2px rgba(0,0,0,.06); + background:clear; + color: white; + width:100%; + height:50%; + -webkit-user-select: none; + pointer-events: none; + content:attr(data-failed); +} + +span.video_container.failed.largeFail::after { + padding:62px 0 0 0; +} + +span.video_container.failed.smallFail::after { + font-family:OpenSans; + content:attr(data-failed); +} + +/* Image and video editing overlay styles */ .edit-container { position: relative; display: inline-block; @@ -210,6 +295,13 @@ span.img_container.failed.smallFail::after { padding:1px; } + +.edit-container video { + -webkit-filter: blur(4px) grayscale(0.3); + margin:-1px; /*tiny margin to keep crisp edges when blurring the image*/ + padding:1px; +} + /* default. use when images are > 100px w/h */ .edit-container .edit-overlay { position: absolute; diff --git a/libs/editor-common/assets/wpposter.svg b/libs/editor-common/assets/wpposter.svg new file mode 100644 index 000000000000..b15c551593df --- /dev/null +++ b/libs/editor-common/assets/wpposter.svg @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file From 097b51b9618f0fe23146c66b58918651e9c80bbc Mon Sep 17 00:00:00 2001 From: Maxime Biais Date: Mon, 15 Feb 2016 09:21:04 +0100 Subject: [PATCH 2/6] Squashed 'libs/editor/' changes from d99856b..69aaa8a 69aaa8a rename Media/Image events adc9012 Replace optional listener by a onTrackableEvent method in the main listener cd16322 move EditorFragmentOptionalListener to a subclass 4b144de remove analytics - use an optional listener instead git-subtree-dir: libs/editor git-subtree-split: 69aaa8aa8ee30816d72f14acb340f7645a368d40 --- WordPressEditor/build.gradle | 1 - .../android/editor/EditorFragment.java | 36 +++++++++---------- .../editor/EditorFragmentAbstract.java | 23 ++++++++++-- .../android/editor/LegacyEditorFragment.java | 18 +++++----- .../example/EditorExampleActivity.java | 8 +++++ 5 files changed, 54 insertions(+), 32 deletions(-) diff --git a/WordPressEditor/build.gradle b/WordPressEditor/build.gradle index df07667ac968..bb309fceca76 100644 --- a/WordPressEditor/build.gradle +++ b/WordPressEditor/build.gradle @@ -48,7 +48,6 @@ android { dependencies { compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.android.support:support-v4:23.1.1' - compile 'org.wordpress:analytics:1.2.0' compile 'org.wordpress:utils:1.7.0' // Test libraries 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 9366b73def16..a2f3fc54d363 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java @@ -32,8 +32,6 @@ import org.json.JSONException; import org.json.JSONObject; -import org.wordpress.android.analytics.AnalyticsTracker; -import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.JSONUtils; @@ -389,7 +387,7 @@ protected void initJsEditor() { public void onClick(View v) { int id = v.getId(); if (id == R.id.format_bar_button_html) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_HTML); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.HTML_BUTTON_TAPPED); // Don't switch to HTML mode if currently uploading media if (!mUploadingMediaIds.isEmpty()) { @@ -431,7 +429,7 @@ public void onClick(View v) { mWebView.execJavaScriptFromString("ZSSEditor.getField('zss_field_content').focus();"); } } else if (id == R.id.format_bar_button_media) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_IMAGE); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.MEDIA_BUTTON_TAPPED); ((ToggleButton) v).setChecked(false); if (mSourceView.getVisibility() == View.VISIBLE) { @@ -446,10 +444,10 @@ public void onClick(View v) { if (!((ToggleButton) v).isChecked()) { // The link button was checked when it was pressed; remove the current link mWebView.execJavaScriptFromString("ZSSEditor.unlink();"); - AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNLINK); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.UNLINK_BUTTON_TAPPED); return; } - AnalyticsTracker.track(Stat.EDITOR_TAPPED_LINK); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.LINK_BUTTON_TAPPED); ((ToggleButton) v).setChecked(false); @@ -740,13 +738,13 @@ public void run() { if (URLUtil.isNetworkUrl(mediaUrl)) { String mediaId = mediaFile.getMediaId(); mWebView.execJavaScriptFromString("ZSSEditor.insertImage('" + mediaUrl + "', '" + mediaId + "');"); - AnalyticsTracker.track(Stat.EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.NETWORK_IMAGE_ADDED); } else { String id = mediaFile.getMediaId(); mWebView.execJavaScriptFromString("ZSSEditor.insertLocalImage(" + id + ", '" + mediaUrl + "');"); mWebView.execJavaScriptFromString("ZSSEditor.setProgressOnImage(" + id + ", " + 0 + ");"); mUploadingMediaIds.add(id); - AnalyticsTracker.track(Stat.EDITOR_ADDED_PHOTO_VIA_LOCAL_LIBRARY); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.LOCAL_IMAGE_ADDED); } } }); @@ -834,7 +832,7 @@ public void onMediaUploadFailed(final String mediaId) { mWebView.post(new Runnable() { @Override public void run() { - AnalyticsTracker.track(Stat.EDITOR_UPLOAD_MEDIA_FAILED); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.UPLOAD_IMAGE_FAILED); mWebView.execJavaScriptFromString("ZSSEditor.markImageUploadFailed(" + mediaId + ");"); mFailedMediaIds.add(mediaId); mUploadingMediaIds.remove(mediaId); @@ -993,7 +991,7 @@ public void onClick(DialogInterface dialog, int id) { mWebView.post(new Runnable() { @Override public void run() { - AnalyticsTracker.track(Stat.EDITOR_UPLOAD_MEDIA_RETRIED); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.UPLOAD_IMAGE_RETRIED); mWebView.execJavaScriptFromString("ZSSEditor.unmarkImageUploadFailed(" + mediaId + ");"); mWebView.execJavaScriptFromString("ZSSEditor.setProgressOnImage(" + mediaId + ", " + 0 + ");"); mFailedMediaIds.remove(mediaId); @@ -1008,7 +1006,7 @@ public void run() { if (fragmentManager.findFragmentByTag(ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG) != null) { return; } - AnalyticsTracker.track(Stat.EDITOR_EDITED_IMAGE); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.IMAGE_EDITED); ImageSettingsDialogFragment imageSettingsDialogFragment = new ImageSettingsDialogFragment(); imageSettingsDialogFragment.setTargetFragment(this, ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_REQUEST_CODE); @@ -1175,7 +1173,7 @@ private void clearFormatBarButtons() { private void onFormattingButtonClicked(ToggleButton toggleButton) { String tag = toggleButton.getTag().toString(); - trackFormattingButtonClicked(toggleButton); + buttonTappedListener(toggleButton); if (mWebView.getVisibility() == View.VISIBLE) { mWebView.execJavaScriptFromString("ZSSEditor.set" + StringUtils.capitalize(tag) + "();"); } else { @@ -1183,20 +1181,20 @@ private void onFormattingButtonClicked(ToggleButton toggleButton) { } } - private void trackFormattingButtonClicked(ToggleButton toggleButton) { + private void buttonTappedListener(ToggleButton toggleButton) { int id = toggleButton.getId(); if (id == R.id.format_bar_button_bold) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_BOLD); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.BOLD_BUTTON_TAPPED); } else if (id == R.id.format_bar_button_italic) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_ITALIC); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.ITALIC_BUTTON_TAPPED); } else if (id == R.id.format_bar_button_ol) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_ORDERED_LIST); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.OL_BUTTON_TAPPED); } else if (id == R.id.format_bar_button_ul) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNORDERED_LIST); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.UL_BUTTON_TAPPED); } else if (id == R.id.format_bar_button_quote) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_BLOCKQUOTE); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.BLOCKQUOTE_BUTTON_TAPPED); } else if (id == R.id.format_bar_button_strikethrough) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_STRIKETHROUGH); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.STRIKETHROUGH_BUTTON_TAPPED); } } 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 ce874bd49a91..d32294200e49 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java @@ -3,7 +3,6 @@ import android.app.Activity; import android.app.Fragment; import android.os.Bundle; - import android.text.Spanned; import com.android.volley.toolbox.ImageLoader; @@ -12,7 +11,6 @@ import org.wordpress.android.util.helpers.MediaGallery; import java.util.HashMap; -import java.util.Map; public abstract class EditorFragmentAbstract extends Fragment { public abstract void setTitle(CharSequence text); @@ -131,5 +129,26 @@ public interface EditorFragmentListener { String onAuthHeaderRequested(String url); // TODO: remove saveMediaFile, it's currently needed for the legacy editor void saveMediaFile(MediaFile mediaFile); + void onTrackableEvent(TrackableEvent event); + } + + public enum TrackableEvent { + HTML_BUTTON_TAPPED, + UNLINK_BUTTON_TAPPED, + LINK_BUTTON_TAPPED, + MEDIA_BUTTON_TAPPED, + NETWORK_IMAGE_ADDED, + LOCAL_IMAGE_ADDED, + UPLOAD_IMAGE_FAILED, + UPLOAD_IMAGE_RETRIED, + IMAGE_EDITED, + BOLD_BUTTON_TAPPED, + ITALIC_BUTTON_TAPPED, + OL_BUTTON_TAPPED, + UL_BUTTON_TAPPED, + BLOCKQUOTE_BUTTON_TAPPED, + STRIKETHROUGH_BUTTON_TAPPED, + UNDERLINE_BUTTON_TAPPED, + MORE_BUTTON_TAPPED } } 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 3ca172e685a6..2bd81b92d720 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/LegacyEditorFragment.java @@ -54,8 +54,6 @@ import com.android.volley.VolleyError; import com.android.volley.toolbox.ImageLoader; -import org.wordpress.android.analytics.AnalyticsTracker; -import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.editor.legacy.EditLinkActivity; import org.wordpress.android.editor.legacy.WPEditImageSpan; import org.wordpress.android.util.AppLog; @@ -415,22 +413,22 @@ private void createLinkFromSelection(String linkURL, String linkText) { public void onClick(View v) { int id = v.getId(); if (id == R.id.bold) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_BOLD); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.BOLD_BUTTON_TAPPED); onFormatButtonClick(mBoldToggleButton, TAG_FORMAT_BAR_BUTTON_STRONG); } else if (id == R.id.em) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_ITALIC); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.ITALIC_BUTTON_TAPPED); onFormatButtonClick(mEmToggleButton, TAG_FORMAT_BAR_BUTTON_EM); } else if (id == R.id.underline) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNDERLINE); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.UNDERLINE_BUTTON_TAPPED); onFormatButtonClick(mUnderlineToggleButton, TAG_FORMAT_BAR_BUTTON_UNDERLINE); } else if (id == R.id.strike) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_STRIKETHROUGH); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.STRIKETHROUGH_BUTTON_TAPPED); onFormatButtonClick(mStrikeToggleButton, TAG_FORMAT_BAR_BUTTON_STRIKE); } else if (id == R.id.bquote) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_BLOCKQUOTE); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.BLOCKQUOTE_BUTTON_TAPPED); onFormatButtonClick(mBquoteToggleButton, TAG_FORMAT_BAR_BUTTON_QUOTE); } else if (id == R.id.more) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_MORE); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.MORE_BUTTON_TAPPED); mSelectionEnd = mContentEditText.getSelectionEnd(); Editable str = mContentEditText.getText(); if (str != null) { @@ -439,7 +437,7 @@ public void onClick(View v) { str.insert(mSelectionEnd, "\n\n"); } } else if (id == R.id.link) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_LINK); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.LINK_BUTTON_TAPPED); mSelectionStart = mContentEditText.getSelectionStart(); mStyleStart = mSelectionStart; mSelectionEnd = mContentEditText.getSelectionEnd(); @@ -457,7 +455,7 @@ public void onClick(View v) { } startActivityForResult(i, ACTIVITY_REQUEST_CODE_CREATE_LINK); } else if (id == R.id.addPictureButton) { - AnalyticsTracker.track(Stat.EDITOR_TAPPED_IMAGE); + mEditorFragmentListener.onTrackableEvent(TrackableEvent.MEDIA_BUTTON_TAPPED); mEditorFragmentListener.onAddMediaClicked(); if (isAdded()) { getActivity().openContextMenu(mAddPictureButton); diff --git a/example/src/main/java/org/wordpress/example/EditorExampleActivity.java b/example/src/main/java/org/wordpress/example/EditorExampleActivity.java index 0edcce21abae..cf7d0ed300d4 100644 --- a/example/src/main/java/org/wordpress/example/EditorExampleActivity.java +++ b/example/src/main/java/org/wordpress/example/EditorExampleActivity.java @@ -11,8 +11,11 @@ import org.wordpress.android.editor.EditorFragmentAbstract; import org.wordpress.android.editor.EditorFragmentAbstract.EditorFragmentListener; +import org.wordpress.android.editor.EditorFragmentAbstract.TrackableEvent; import org.wordpress.android.editor.EditorMediaUploadListener; import org.wordpress.android.editor.ImageSettingsDialogFragment; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.helpers.MediaFile; @@ -195,6 +198,11 @@ public void saveMediaFile(MediaFile mediaFile) { // TODO } + @Override + public void onTrackableEvent(TrackableEvent event) { + AppLog.d(T.EDITOR, "Trackable event: " + event); + } + private void simulateFileUpload(final String mediaId, final String mediaUrl) { Thread thread = new Thread() { @Override From aa8d9d5b863ae341a6c7774d9398432a2e0eb7de Mon Sep 17 00:00:00 2001 From: Maxime Biais Date: Mon, 15 Feb 2016 09:57:28 +0100 Subject: [PATCH 3/6] Fix media isVideo/isValidImage/... utils method --- .../java/org/wordpress/android/util/MediaUtils.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java index cea4ce917bea..8e2c773339ce 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java @@ -33,7 +33,7 @@ public static boolean isValidImage(String url) { if (url == null) { return false; } - + url = url.toLowerCase(); return url.endsWith(".png") || url.endsWith(".jpg") || url.endsWith(".jpeg") || url.endsWith(".gif"); } @@ -41,7 +41,7 @@ public static boolean isDocument(String url) { if (url == null) { return false; } - + url = url.toLowerCase(); return url.endsWith(".doc") || url.endsWith(".docx") || url.endsWith(".odt") || url.endsWith(".pdf"); } @@ -49,7 +49,7 @@ public static boolean isPowerpoint(String url) { if (url == null) { return false; } - + url = url.toLowerCase(); return url.endsWith(".ppt") || url.endsWith(".pptx") || url.endsWith(".pps") || url.endsWith(".ppsx") || url.endsWith(".key"); } @@ -58,7 +58,7 @@ public static boolean isSpreadsheet(String url) { if (url == null) { return false; } - + url = url.toLowerCase(); return url.endsWith(".xls") || url.endsWith(".xlsx"); } @@ -66,6 +66,7 @@ public static boolean isVideo(String url) { if (url == null) { return false; } + url = url.toLowerCase(); return url.endsWith(".ogv") || url.endsWith(".mp4") || url.endsWith(".m4v") || url.endsWith(".mov") || url.endsWith(".wmv") || url.endsWith(".avi") || url.endsWith(".mpg") || url.endsWith(".3gp") || url.endsWith(".3g2") || url.contains("video"); @@ -75,6 +76,7 @@ public static boolean isAudio(String url) { if (url == null) { return false; } + url = url.toLowerCase(); return url.endsWith(".mp3") || url.endsWith(".ogg") || url.endsWith(".wav") || url.endsWith(".wma") || url.endsWith(".aiff") || url.endsWith(".aif") || url.endsWith(".aac") || url.endsWith(".m4a"); } From 68c6f4cd95baefe3198dca9e8bd7d1ee3732f576 Mon Sep 17 00:00:00 2001 From: Maxime Biais Date: Tue, 16 Feb 2016 14:37:53 +0100 Subject: [PATCH 4/6] Squashed 'libs/editor/' changes from 69aaa8a..4832795 4832795 Merge pull request #280 from wordpress-mobile/issue/remove-analytics-dependency dd68ccd Merge pull request #283 from wordpress-mobile/issue/282-localize-js-strings bec42a8 moved the brackets back into the JS code 2cd15b7 fix #282: localize JS strings by using js-java interface nativeState a64d3a7 Merge pull request #270 from wordpress-mobile/issue/242-image-container-lower-api 5339a22 Fixed some EditorFragment lint issues ab084be Used a String constant for naming the native state JS interface 0789069 Merge branch 'develop' into issue/242-image-container-lower-api 430d3a4 Merge pull request #278 from wordpress-mobile/issue/183-accessibility e4b9e76 s/Strike/Strikethrough/ ec9c6e9 Image thumbnail content description cda2f22 Make editor title i18nizable 9d92717 Merge branch 'develop' into issue/242-image-container-lower-api 67dec0b Change page title for accessibility 3167e9a add contentDescription to format bar buttons in the visual and legacy editors 29cea46 Fixed issue where compatibility upload overlays would sometimes consume a tap without triggering a tap callback for the associated image 7b26aa7 Fixed a display issue on API<19 where a dark blank space would appear below an uploading or failed image b95d94a Added a dark semi-transparent overlay for API<19 for uploading and failed images 9e1863f Revert to non-compatibility image container styling once image has begun uploading 34523c0 Hide the compatibility image upload overlay if an image upload fails and re-display it on resume abf7260 Use block instead of inline-block for the image container on pre=KitKat Android 86a985c On pre-KitKat Android, show an 'Uploading...' overlay instead of a progress bar for uploading images 047082a Updated ZSSEditor to get the current Android API level on init git-subtree-dir: libs/editor git-subtree-split: 48327953c76c593daefe93a16e41323631de5b73 --- .../android/editor/EditorFragment.java | 48 +++++++++++++++-- .../android/editor/RippleToggleButton.java | 7 ++- .../src/main/res/layout-w360dp/format_bar.xml | 10 +++- .../src/main/res/layout-w380dp/format_bar.xml | 10 +++- .../src/main/res/layout-w600dp/format_bar.xml | 8 +++ .../main/res/layout/dialog_image_options.xml | 3 +- .../src/main/res/layout/format_bar.xml | 10 +++- .../res/layout/fragment_edit_post_content.xml | 8 +++ .../src/main/res/values/strings.xml | 20 +++++++ .../editor-common/assets/ZSSRichTextEditor.js | 53 ++++++++++++++++--- libs/editor-common/assets/android-editor.html | 2 +- libs/editor-common/assets/editor-android.css | 52 ++++++++++++++++++ 12 files changed, 211 insertions(+), 20 deletions(-) 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 a2f3fc54d363..ad019edb4e84 100755 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/EditorFragment.java @@ -24,6 +24,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; +import android.webkit.JavascriptInterface; import android.webkit.URLUtil; import android.webkit.WebView; import android.widget.ToggleButton; @@ -59,6 +60,7 @@ public class EditorFragment extends EditorFragmentAbstract implements View.OnCli private static final String ARG_PARAM_CONTENT = "param_content"; private static final String JS_CALLBACK_HANDLER = "nativeCallbackHandler"; + private static final String JS_STATE_INTERFACE = "nativeState"; private static final String KEY_TITLE = "title"; private static final String KEY_CONTENT = "content"; @@ -366,13 +368,18 @@ private void setupFormatBarButtonMap(View view) { } protected void initJsEditor() { - if(!isAdded()) { + if (!isAdded()) { return; } String htmlEditor = Utils.getHtmlFromFile(getActivity(), "android-editor.html"); + if (htmlEditor != null) { + htmlEditor = htmlEditor.replace("%%TITLE%%", getString(R.string.visual_editor)); + } mWebView.addJavascriptInterface(new JsCallbackReceiver(this), JS_CALLBACK_HANDLER); + mWebView.addJavascriptInterface(new NativeStateJsInterface(getActivity().getApplicationContext()), + JS_STATE_INTERFACE); mWebView.loadDataWithBaseURL("file:///android_asset/", htmlEditor, "text/html", "utf-8", ""); @@ -1123,7 +1130,7 @@ private void updateVisualEditorFields() { private void hideActionBarIfNeeded() { ActionBar actionBar = getActionBar(); - if (getActionBar() != null + if (actionBar != null && !isHardwareKeyboardPresent() && mHideActionBarOnSoftKeyboardUp && mIsKeyboardOpen @@ -1138,8 +1145,8 @@ private void hideActionBarIfNeeded() { private void showActionBarIfNeeded() { ActionBar actionBar = getActionBar(); - if (getActionBar() != null && !actionBar.isShowing()) { - getActionBar().show(); + if (actionBar != null && !actionBar.isShowing()) { + actionBar.show(); } } @@ -1261,4 +1268,37 @@ private void applyFormattingHtmlMode(ToggleButton toggleButton, String tag) { mSourceViewContent.setSelection(selectionEnd + endTag.length()); } } + + private class NativeStateJsInterface { + Context mContext; + + NativeStateJsInterface(Context context) { + mContext = context; + } + + @JavascriptInterface + public String getStringEdit() { + return mContext.getString(R.string.edit); + } + + @JavascriptInterface + public String getStringTapToRetry() { + return mContext.getString(R.string.tap_to_try_again); + } + + @JavascriptInterface + public String getStringUploading() { + return mContext.getString(R.string.uploading); + } + + @JavascriptInterface + public String getStringUploadingGallery() { + return mContext.getString(R.string.uploading_gallery_placeholder); + } + + @JavascriptInterface + public int getAPILevel() { + return Build.VERSION.SDK_INT; + } + } } diff --git a/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java b/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java index 4d932b322e0b..fd2ac6110142 100644 --- a/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java +++ b/WordPressEditor/src/main/java/org/wordpress/android/editor/RippleToggleButton.java @@ -21,12 +21,11 @@ public class RippleToggleButton extends ToggleButton { private Paint mStrokePaint; public RippleToggleButton(Context context) { - super(context); - } + this(context, null); + } public RippleToggleButton(Context context, AttributeSet attrs) { - super(context, attrs); - init(); + this(context, attrs, 0); } public RippleToggleButton(Context context, AttributeSet attrs, int defStyle) { diff --git a/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml b/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml index 6b498fe0baee..fc2eb283e5f0 100644 --- a/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml +++ b/WordPressEditor/src/main/res/layout-w360dp/format_bar.xml @@ -37,6 +37,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_media" android:background="@drawable/format_bar_button_media_selector"/> @@ -52,6 +54,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_italic" android:background="@drawable/format_bar_button_italic_selector" android:tag="@string/format_bar_tag_italic"/> @@ -60,6 +63,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_quote" android:background="@drawable/format_bar_button_quote_selector" android:tag="@string/format_bar_tag_blockquote"/> @@ -68,6 +72,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_ul" android:background="@drawable/format_bar_button_ul_selector" android:tag="@string/format_bar_tag_unorderedList"/> @@ -76,6 +81,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_ol" android:background="@drawable/format_bar_button_ol_selector" android:tag="@string/format_bar_tag_orderedList"/> @@ -84,6 +90,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_link" android:background="@drawable/format_bar_button_link_selector" android:tag="@string/format_bar_tag_link"/> @@ -100,6 +107,7 @@ style="@style/FormatBarHtmlButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_html" android:background="@drawable/format_bar_button_html_selector"/> - \ No newline at end of file + diff --git a/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml b/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml index bd8e1f900644..17d9f6d26358 100644 --- a/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml +++ b/WordPressEditor/src/main/res/layout-w380dp/format_bar.xml @@ -31,6 +31,7 @@ android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_weight="1" + android:contentDescription="@string/format_bar_description_media" android:background="@drawable/format_bar_button_media_selector"/> @@ -48,6 +50,7 @@ android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_weight="1" + android:contentDescription="@string/format_bar_description_italic" android:background="@drawable/format_bar_button_italic_selector" android:tag="@string/format_bar_tag_italic"/> @@ -57,6 +60,7 @@ android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_weight="1" + android:contentDescription="@string/format_bar_description_quote" android:background="@drawable/format_bar_button_quote_selector" android:tag="@string/format_bar_tag_blockquote"/> @@ -66,6 +70,7 @@ android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_weight="1" + android:contentDescription="@string/format_bar_description_ul" android:background="@drawable/format_bar_button_ul_selector" android:tag="@string/format_bar_tag_unorderedList"/> @@ -75,6 +80,7 @@ android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_weight="1" + android:contentDescription="@string/format_bar_description_ol" android:background="@drawable/format_bar_button_ol_selector" android:tag="@string/format_bar_tag_orderedList"/> @@ -84,6 +90,7 @@ android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_weight="1" + android:contentDescription="@string/format_bar_description_link" android:background="@drawable/format_bar_button_link_selector" android:tag="@string/format_bar_tag_link"/> @@ -99,6 +106,7 @@ style="@style/FormatBarHtmlButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_html" android:background="@drawable/format_bar_button_html_selector"/> - \ No newline at end of file + diff --git a/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml b/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml index dcceb2ea9321..58f907084759 100644 --- a/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml +++ b/WordPressEditor/src/main/res/layout-w600dp/format_bar.xml @@ -34,6 +34,7 @@ style="@style/FormatBarButtonTablet" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_media" android:background="@drawable/format_bar_button_media_selector"/> @@ -47,6 +48,7 @@ style="@style/FormatBarButtonTablet" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_bold" android:background="@drawable/format_bar_button_bold_selector" android:tag="@string/format_bar_tag_bold"/> @@ -55,6 +57,7 @@ style="@style/FormatBarButtonTablet" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_italic" android:background="@drawable/format_bar_button_italic_selector" android:tag="@string/format_bar_tag_italic"/> @@ -77,6 +80,7 @@ style="@style/FormatBarButtonTablet" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_link" android:background="@drawable/format_bar_button_link_selector" android:tag="@string/format_bar_tag_link"/> @@ -91,6 +95,7 @@ style="@style/FormatBarButtonTablet" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_ul" android:background="@drawable/format_bar_button_ul_selector" android:tag="@string/format_bar_tag_unorderedList"/> @@ -99,6 +104,7 @@ style="@style/FormatBarButtonTablet" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_ol" android:background="@drawable/format_bar_button_ol_selector" android:tag="@string/format_bar_tag_orderedList"/> @@ -107,6 +113,7 @@ style="@style/FormatBarButtonTablet" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_quote" android:background="@drawable/format_bar_button_quote_selector" android:tag="@string/format_bar_tag_blockquote"/> @@ -121,6 +128,7 @@ style="@style/FormatBarButtonTablet" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_html" android:background="@drawable/format_bar_button_html_selector"/> diff --git a/WordPressEditor/src/main/res/layout/dialog_image_options.xml b/WordPressEditor/src/main/res/layout/dialog_image_options.xml index 9b471f50832f..ead7e7653d37 100644 --- a/WordPressEditor/src/main/res/layout/dialog_image_options.xml +++ b/WordPressEditor/src/main/res/layout/dialog_image_options.xml @@ -21,6 +21,7 @@ - \ No newline at end of file + diff --git a/WordPressEditor/src/main/res/layout/format_bar.xml b/WordPressEditor/src/main/res/layout/format_bar.xml index a8e3e57a09af..5fb9c4fabe21 100644 --- a/WordPressEditor/src/main/res/layout/format_bar.xml +++ b/WordPressEditor/src/main/res/layout/format_bar.xml @@ -42,6 +42,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_media" android:background="@drawable/format_bar_button_media_selector"/> @@ -57,6 +59,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_italic" android:background="@drawable/format_bar_button_italic_selector" android:tag="@string/format_bar_tag_italic"/> @@ -65,6 +68,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_quote" android:background="@drawable/format_bar_button_quote_selector" android:tag="@string/format_bar_tag_blockquote"/> @@ -73,6 +77,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_ul" android:background="@drawable/format_bar_button_ul_selector" android:tag="@string/format_bar_tag_unorderedList"/> @@ -81,6 +86,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_ol" android:background="@drawable/format_bar_button_ol_selector" android:tag="@string/format_bar_tag_orderedList"/> @@ -89,6 +95,7 @@ style="@style/FormatBarButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_link" android:background="@drawable/format_bar_button_link_selector" android:tag="@string/format_bar_tag_link"/> @@ -106,6 +113,7 @@ style="@style/FormatBarHtmlButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_html" android:background="@drawable/format_bar_button_html_selector"/> - \ No newline at end of file + diff --git a/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml b/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml index 76fa09b2caa3..c94a08e5e27e 100644 --- a/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml +++ b/WordPressEditor/src/main/res/layout/fragment_edit_post_content.xml @@ -100,6 +100,7 @@ style="@style/LegacyToggleButton" android:layout_width="wrap_content" android:layout_height="fill_parent" + android:contentDescription="@string/format_bar_description_bold" android:background="@drawable/legacy_format_bar_button_bold_selector" />