package org.wordpress.android.ui.posts;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v13.app.FragmentPagerAdapter;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.CursorLoader;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.SuggestionSpan;
import android.view.DragEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.wordpress.android.BuildConfig;
import org.wordpress.android.JavaScriptException;
import org.wordpress.android.R;
import org.wordpress.android.WordPress;
import org.wordpress.android.analytics.AnalyticsTracker;
import org.wordpress.android.analytics.AnalyticsTracker.Stat;
import org.wordpress.android.editor.AztecEditorFragment;
import org.wordpress.android.editor.EditorFragment;
import org.wordpress.android.editor.EditorFragment.IllegalEditorStateException;
import org.wordpress.android.editor.EditorFragmentAbstract;
import org.wordpress.android.editor.EditorFragmentAbstract.EditorDragAndDropListener;
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.EditorWebViewAbstract.ErrorListener;
import org.wordpress.android.editor.EditorWebViewCompatibility;
import org.wordpress.android.editor.EditorWebViewCompatibility.ReflectionException;
import org.wordpress.android.editor.ImageSettingsDialogFragment;
import org.wordpress.android.editor.LegacyEditorFragment;
import org.wordpress.android.fluxc.Dispatcher;
import org.wordpress.android.fluxc.action.AccountAction;
import org.wordpress.android.fluxc.generated.AccountActionBuilder;
import org.wordpress.android.fluxc.generated.MediaActionBuilder;
import org.wordpress.android.fluxc.generated.PostActionBuilder;
import org.wordpress.android.fluxc.model.AccountModel;
import org.wordpress.android.fluxc.model.MediaModel;
import org.wordpress.android.fluxc.model.MediaModel.UploadState;
import org.wordpress.android.fluxc.model.PostModel;
import org.wordpress.android.fluxc.model.SiteModel;
import org.wordpress.android.fluxc.model.post.PostStatus;
import org.wordpress.android.fluxc.store.AccountStore;
import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged;
import org.wordpress.android.fluxc.store.MediaStore;
import org.wordpress.android.fluxc.store.MediaStore.FetchMediaListPayload;
import org.wordpress.android.fluxc.store.MediaStore.MediaPayload;
import org.wordpress.android.fluxc.store.MediaStore.OnMediaChanged;
import org.wordpress.android.fluxc.store.PostStore;
import org.wordpress.android.fluxc.store.SiteStore;
import org.wordpress.android.fluxc.tools.FluxCImageLoader;
import org.wordpress.android.ui.ActivityId;
import org.wordpress.android.ui.ActivityLauncher;
import org.wordpress.android.ui.RequestCodes;
import org.wordpress.android.ui.media.MediaGalleryActivity;
import org.wordpress.android.ui.media.MediaGalleryPickerActivity;
import org.wordpress.android.ui.media.WordPressMediaUtils;
import org.wordpress.android.ui.media.services.MediaUploadService;
import org.wordpress.android.ui.notifications.utils.PendingDraftsNotificationsUtils;
import org.wordpress.android.ui.photopicker.PhotoPickerFragment;
import org.wordpress.android.ui.photopicker.PhotoPickerFragment.PhotoPickerIcon;
import org.wordpress.android.ui.photopicker.PhotoPickerFragment.PhotoPickerOption;
import org.wordpress.android.ui.posts.InsertMediaDialog.InsertMediaCallback;
import org.wordpress.android.ui.posts.services.AztecImageLoader;
import org.wordpress.android.ui.posts.services.PostEvents;
import org.wordpress.android.ui.posts.services.PostUploadService;
import org.wordpress.android.ui.prefs.AppPrefs;
import org.wordpress.android.ui.prefs.SiteSettingsInterface;
import org.wordpress.android.util.AnalyticsUtils;
import org.wordpress.android.util.AniUtils;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.AppLog.T;
import org.wordpress.android.util.AutolinkUtils;
import org.wordpress.android.util.CrashlyticsUtils;
import org.wordpress.android.util.DateTimeUtils;
import org.wordpress.android.util.DisplayUtils;
import org.wordpress.android.util.FluxCUtils;
import org.wordpress.android.util.ImageUtils;
import org.wordpress.android.util.ListUtils;
import org.wordpress.android.util.MediaUtils;
import org.wordpress.android.util.NetworkUtils;
import org.wordpress.android.util.PermissionUtils;
import org.wordpress.android.util.SiteUtils;
import org.wordpress.android.util.SmartToast;
import org.wordpress.android.util.StringUtils;
import org.wordpress.android.util.ToastUtils;
import org.wordpress.android.util.ToastUtils.Duration;
import org.wordpress.android.util.WPHtml;
import org.wordpress.android.util.WPMediaUtils;
import org.wordpress.android.util.WPUrlUtils;
import org.wordpress.android.util.helpers.MediaFile;
import org.wordpress.android.util.helpers.MediaGallery;
import org.wordpress.android.util.helpers.MediaGalleryImageSpan;
import org.wordpress.android.util.helpers.WPImageSpan;
import org.wordpress.android.widgets.WPViewPager;
import org.wordpress.passcodelock.AppLockManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import de.greenrobot.event.EventBus;
public class EditPostActivity extends AppCompatActivity implements EditorFragmentListener, EditorDragAndDropListener,
ActivityCompat.OnRequestPermissionsResultCallback, EditorWebViewCompatibility.ReflectionFailureListener,
PhotoPickerFragment.PhotoPickerListener {
public static final String EXTRA_POST = "postModel";
public static final String EXTRA_IS_PAGE = "isPage";
public static final String EXTRA_IS_QUICKPRESS = "isQuickPress";
public static final String EXTRA_QUICKPRESS_BLOG_ID = "quickPressBlogId";
public static final String EXTRA_SAVED_AS_LOCAL_DRAFT = "savedAsLocalDraft";
public static final String EXTRA_HAS_UNFINISHED_MEDIA = "hasUnfinishedMedia";
public static final String EXTRA_HAS_CHANGES = "hasChanges";
public static final String STATE_KEY_CURRENT_POST = "stateKeyCurrentPost";
public static final String STATE_KEY_ORIGINAL_POST = "stateKeyOriginalPost";
public static final String STATE_KEY_EDITOR_FRAGMENT = "editorFragment";
public static final String STATE_KEY_DROPPED_MEDIA_URIS = "stateKeyDroppedMediaUri";
public static final int MEDIA_PERMISSION_REQUEST_CODE = 1;
public static final int LOCATION_PERMISSION_REQUEST_CODE = 2;
public static final int DRAG_AND_DROP_MEDIA_PERMISSION_REQUEST_CODE = 3;
public static final int PHOTO_PICKER_PERMISSION_REQUEST_CODE = 4;
private static int PAGE_CONTENT = 0;
private static int PAGE_SETTINGS = 1;
private static int PAGE_PREVIEW = 2;
private static final int AUTOSAVE_INTERVAL_MILLIS = 60000;
private static final String PHOTO_PICKER_TAG = "photo_picker";
private Handler mHandler;
private boolean mShowAztecEditor;
private boolean mShowNewEditor;
private List<String> mPendingVideoPressInfoRequests;
/**
* The {@link android.support.v4.view.PagerAdapter} that will provide
* fragments for each of the sections. We use a
* {@link FragmentPagerAdapter} derivative, which will keep every
* loaded fragment in memory. If this becomes too memory intensive, it
* may be best to switch to a
* {@link android.support.v13.app.FragmentStatePagerAdapter}.
*/
SectionsPagerAdapter mSectionsPagerAdapter;
/**
* The {@link ViewPager} that will host the section contents.
*/
WPViewPager mViewPager;
private PostModel mPost;
private PostModel mOriginalPost;
private AztecEditorFragment mAztecEditorFragment;
private EditorFragmentAbstract mEditorFragment;
private EditPostSettingsFragment mEditPostSettingsFragment;
private EditPostPreviewFragment mEditPostPreviewFragment;
private EditorMediaUploadListener mEditorMediaUploadListener;
private boolean mIsNewPost;
private boolean mIsPage;
private boolean mHasSetPostContent;
private View mPhotoPickerContainer;
private PhotoPickerFragment mPhotoPickerFragment;
private int mPhotoPickerOrientation = Configuration.ORIENTATION_UNDEFINED;
// For opening the context menu after permissions have been granted
private View mMenuView = null;
@Inject Dispatcher mDispatcher;
@Inject AccountStore mAccountStore;
@Inject SiteStore mSiteStore;
@Inject PostStore mPostStore;
@Inject MediaStore mMediaStore;
@Inject FluxCImageLoader mImageLoader;
private SiteModel mSite;
// for keeping the media uri while asking for permissions
private ArrayList<Uri> mDroppedMediaUris;
private Runnable mFetchMediaRunnable = new Runnable() {
@Override
public void run() {
if (mDroppedMediaUris != null) {
final List<Uri> mediaUris = mDroppedMediaUris;
mDroppedMediaUris = null;
EditPostActivity.this.addAllMedia(mediaUris);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((WordPress) getApplication()).component().inject(this);
mDispatcher.register(this);
setContentView(R.layout.new_edit_post_activity);
if (savedInstanceState == null) {
mSite = (SiteModel) getIntent().getSerializableExtra(WordPress.SITE);
} else {
mSite = (SiteModel) savedInstanceState.getSerializable(WordPress.SITE);
}
// Check whether to show the visual editor
PreferenceManager.setDefaultValues(this, R.xml.account_settings, false);
//AppPrefs.setAztecEditorAvailable(true);
//AppPrefs.setAztecEditorEnabled(true);
mShowAztecEditor = AppPrefs.isAztecEditorEnabled();
mShowNewEditor = AppPrefs.isVisualEditorEnabled();
// Set up the action bar.
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
FragmentManager fragmentManager = getFragmentManager();
Bundle extras = getIntent().getExtras();
String action = getIntent().getAction();
if (savedInstanceState == null) {
if (!getIntent().hasExtra(EXTRA_POST)
|| Intent.ACTION_SEND.equals(action)
|| Intent.ACTION_SEND_MULTIPLE.equals(action)
|| NEW_MEDIA_POST.equals(action)
|| getIntent().hasExtra(EXTRA_IS_QUICKPRESS)) {
if (getIntent().hasExtra(EXTRA_QUICKPRESS_BLOG_ID)) {
// QuickPress might want to use a different blog than the current blog
int localSiteId = getIntent().getIntExtra(EXTRA_QUICKPRESS_BLOG_ID, -1);
mSite = mSiteStore.getSiteByLocalId(localSiteId);
}
if (extras != null) {
mIsPage = extras.getBoolean(EXTRA_IS_PAGE);
}
mIsNewPost = true;
if (mSite == null) {
showErrorAndFinish(R.string.blog_not_found);
return;
}
if (!mSite.isVisible()) {
showErrorAndFinish(R.string.error_blog_hidden);
return;
}
// Create a new post
List<Long> categories = new ArrayList<>();
String postFormat = "";
if (mSite.isWPCom() || mSite.isJetpackConnected()) {
// TODO: replace SiteSettingsInterface.getX by calls to mSite.getDefaultCategory
// and mSite.getDefaultFormat. We can get these from /me/sites endpoint for .com/jetpack sites.
// There might be a way to get that information from a XMLRPC request as well.
categories.add((long) SiteSettingsInterface.getDefaultCategory(WordPress.getContext()));
postFormat = SiteSettingsInterface.getDefaultFormat(WordPress.getContext());
}
mPost = mPostStore.instantiatePostModel(mSite, mIsPage, categories, postFormat);
} else if (extras != null) {
// Load post passed in extras
mPost = (PostModel) extras.getSerializable(EXTRA_POST);
if (mPost != null) {
mOriginalPost = mPost.clone();
mIsPage = mPost.isPage();
}
} else {
// A postId extra must be passed to this activity
showErrorAndFinish(R.string.post_not_found);
return;
}
} else {
mDroppedMediaUris = savedInstanceState.getParcelable(STATE_KEY_DROPPED_MEDIA_URIS);
if (savedInstanceState.containsKey(STATE_KEY_ORIGINAL_POST)) {
try {
mPost = (PostModel) savedInstanceState.getSerializable(STATE_KEY_CURRENT_POST);
mOriginalPost = (PostModel) savedInstanceState.getSerializable(STATE_KEY_ORIGINAL_POST);
} catch (ClassCastException e) {
mPost = null;
}
}
mEditorFragment = (EditorFragmentAbstract) fragmentManager.getFragment(savedInstanceState, STATE_KEY_EDITOR_FRAGMENT);
if (mEditorFragment instanceof EditorMediaUploadListener) {
mEditorMediaUploadListener = (EditorMediaUploadListener) mEditorFragment;
}
}
if (mSite == null) {
ToastUtils.showToast(this, R.string.blog_not_found, ToastUtils.Duration.SHORT);
finish();
return;
}
if (mHasSetPostContent = mEditorFragment != null) {
mEditorFragment.setImageLoader(mImageLoader);
}
// Ensure we have a valid post
if (mPost == null) {
showErrorAndFinish(R.string.post_not_found);
return;
}
if (mIsNewPost) {
trackEditorCreatedPost(action, getIntent());
}
setTitle(StringUtils.unescapeHTML(SiteUtils.getSiteNameOrHomeURL(mSite)));
mSectionsPagerAdapter = new SectionsPagerAdapter(fragmentManager);
// Set up the ViewPager with the sections adapter.
mViewPager = (WPViewPager) findViewById(R.id.pager);
mViewPager.setAdapter(mSectionsPagerAdapter);
mViewPager.setOffscreenPageLimit(2);
mViewPager.setPagingEnabled(false);
// When swiping between different sections, select the corresponding
// tab. We can also use ActionBar.Tab#select() to do this if we have
// a reference to the Tab.
mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
invalidateOptionsMenu();
if (position == PAGE_CONTENT) {
setTitle(StringUtils.unescapeHTML(SiteUtils.getSiteNameOrHomeURL(mSite)));
} else if (position == PAGE_SETTINGS) {
setTitle(mPost.isPage() ? R.string.page_settings : R.string.post_settings);
hidePhotoPicker();
} else if (position == PAGE_PREVIEW) {
setTitle(mPost.isPage() ? R.string.preview_page : R.string.preview_post);
hidePhotoPicker();
savePostAsync(new AfterSavePostListener() {
@Override
public void onPostSave() {
if (mEditPostPreviewFragment != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mEditPostPreviewFragment != null) {
mEditPostPreviewFragment.loadPost();
}
}
});
}
}
});
}
}
});
ActivityId.trackLastActivity(ActivityId.POST_EDITOR);
}
private Runnable mAutoSave = new Runnable() {
@Override
public void run() {
new Thread(new Runnable() {
@Override
public void run() {
try {
updatePostObject(true);
} catch (IllegalEditorStateException e) {
AppLog.e(T.EDITOR, "Impossible to save the post, we weren't able to update it.");
return;
}
savePostToDb();
if (mHandler != null) {
mHandler.postDelayed(mAutoSave, AUTOSAVE_INTERVAL_MILLIS);
}
}
}).start();
}
};
@Override
protected void onResume() {
super.onResume();
mHandler = new Handler();
mHandler.postDelayed(mAutoSave, AUTOSAVE_INTERVAL_MILLIS);
}
@Override
protected void onPause() {
super.onPause();
try {
unregisterReceiver(mGalleryReceiver);
} catch (IllegalArgumentException e) {
AppLog.d(T.EDITOR, "Illegal state! Can't unregister receiver that was no registered");
}
mHandler.removeCallbacks(mAutoSave);
mHandler = null;
}
@Override
protected void onDestroy() {
AnalyticsTracker.track(AnalyticsTracker.Stat.EDITOR_CLOSED);
mDispatcher.unregister(this);
super.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Saves both post objects so we can restore them in onCreate()
savePostAsync(null);
outState.putSerializable(STATE_KEY_CURRENT_POST, mPost);
outState.putSerializable(STATE_KEY_ORIGINAL_POST, mOriginalPost);
outState.putSerializable(WordPress.SITE, mSite);
outState.putParcelableArrayList(STATE_KEY_DROPPED_MEDIA_URIS, mDroppedMediaUris);
if (mEditorFragment != null) {
getFragmentManager().putFragment(outState, STATE_KEY_EDITOR_FRAGMENT, mEditorFragment);
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// resize the photo picker if the user rotated the device
int orientation = newConfig.orientation;
if (orientation != mPhotoPickerOrientation) {
resizePhotoPicker();
}
}
private String getSaveButtonText() {
if (!mSite.getHasCapabilityPublishPosts()) {
return getString(R.string.submit_for_review);
}
switch (PostStatus.fromPost(mPost)) {
case SCHEDULED:
return getString(R.string.schedule_verb);
case PUBLISHED:
case UNKNOWN:
if (mPost.isLocalDraft()) {
return getString(R.string.publish_post);
} else {
return getString(R.string.update_verb);
}
default:
if (mPost.isLocalDraft()) {
return getString(R.string.save);
} else {
return getString(R.string.update_verb);
}
}
}
private boolean isPhotoPickerShowing() {
return mPhotoPickerContainer != null
&& mPhotoPickerContainer.getVisibility() == View.VISIBLE;
}
/*
* resizes the photo picker based on device orientation - full height in landscape, half
* height in portrait
*/
private void resizePhotoPicker() {
if (mPhotoPickerContainer == null) return;
if (DisplayUtils.isLandscape(this)) {
mPhotoPickerOrientation = Configuration.ORIENTATION_LANDSCAPE;
mPhotoPickerContainer.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
} else {
mPhotoPickerOrientation = Configuration.ORIENTATION_PORTRAIT;
int displayHeight = DisplayUtils.getDisplayPixelHeight(this);
int containerHeight = (int) (displayHeight * 0.5f);
mPhotoPickerContainer.getLayoutParams().height = containerHeight;
}
if (mPhotoPickerFragment != null) {
mPhotoPickerFragment.reload();
}
}
/*
* loads the photo picker fragment, which is hidden until the user taps the media icon
*/
private void initPhotoPicker() {
mPhotoPickerContainer = findViewById(R.id.photo_fragment_container);
// size the picker before creating the fragment to avoid having it load media now
resizePhotoPicker();
EnumSet<PhotoPickerOption> options =
EnumSet.of(PhotoPickerOption.ALLOW_MULTI_SELECT);
mPhotoPickerFragment = PhotoPickerFragment.newInstance(this, options);
getFragmentManager()
.beginTransaction()
.add(R.id.photo_fragment_container, mPhotoPickerFragment, PHOTO_PICKER_TAG)
.commit();
}
/*
* user has requested to show the photo picker
*/
void showPhotoPicker() {
// request permissions if we don't already have them
if (!PermissionUtils.checkAndRequestCameraAndStoragePermissions(this, PHOTO_PICKER_PERMISSION_REQUEST_CODE)) {
return;
}
// make sure we initialized the photo picker
if (mPhotoPickerFragment == null) {
initPhotoPicker();
SmartToast.show(this, SmartToast.SmartToastType.PHOTO_PICKER_LONG_PRESS);
}
// hide soft keyboard
View view = getCurrentFocus();
if (view != null) {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
// slide in the photo picker
if (!isPhotoPickerShowing()) {
AniUtils.animateBottomBar(mPhotoPickerContainer, true, AniUtils.Duration.MEDIUM);
mPhotoPickerFragment.refresh();
}
// fade in the overlay atop the editor, which effectively disables the editor
// until the picker is closed
View overlay = findViewById(R.id.view_overlay);
if (overlay.getVisibility() != View.VISIBLE) {
AniUtils.fadeIn(overlay, AniUtils.Duration.MEDIUM);
}
if (mAztecEditorFragment != null) {
mAztecEditorFragment.enableMediaMode(true);
}
}
public void hidePhotoPicker() {
if (isPhotoPickerShowing()) {
mPhotoPickerFragment.finishActionMode();
AniUtils.animateBottomBar(mPhotoPickerContainer, false);
}
View overlay = findViewById(R.id.view_overlay);
if (overlay.getVisibility() == View.VISIBLE) {
AniUtils.fadeOut(overlay, AniUtils.Duration.MEDIUM);
}
if (mAztecEditorFragment != null) {
mAztecEditorFragment.enableMediaMode(false);
}
}
/*
* called by PhotoPickerFragment when media is selected - may be a single item or a list of items
*/
@Override
public void onPhotoPickerMediaChosen(@NonNull List<Uri> uriList) {
hidePhotoPicker();
for (Uri uri: uriList) {
if (addMedia(uri)) {
boolean isVideo = MediaUtils.isVideo(uri.toString());
trackAddMediaFromDeviceEvents(false, isVideo, uri);
}
}
}
/*
* called by PhotoPickerFragment when user clicks an icon to launch the camera, native
* picker, or WP media picker
*/
@Override
public void onPhotoPickerIconClicked(@NonNull PhotoPickerIcon icon) {
hidePhotoPicker();
switch (icon) {
case ANDROID_CAPTURE_PHOTO:
launchCamera();
break;
case ANDROID_CAPTURE_VIDEO:
launchVideoCamera();
break;
case ANDROID_CHOOSE_PHOTO:
launchPictureLibrary();
break;
case ANDROID_CHOOSE_VIDEO:
launchVideoLibrary();
break;
case WP_MEDIA:
startMediaGalleryAddActivity();
break;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
if (mShowNewEditor || mShowAztecEditor) {
inflater.inflate(R.menu.edit_post, menu);
} else {
inflater.inflate(R.menu.edit_post_legacy, menu);
}
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean showMenuItems = true;
if (mViewPager != null && mViewPager.getCurrentItem() > PAGE_CONTENT) {
showMenuItems = false;
}
MenuItem previewMenuItem = menu.findItem(R.id.menu_preview_post);
MenuItem settingsMenuItem = menu.findItem(R.id.menu_post_settings);
if (previewMenuItem != null) {
previewMenuItem.setVisible(showMenuItems);
}
if (settingsMenuItem != null) {
settingsMenuItem.setVisible(showMenuItems);
}
// Set text of the save button in the ActionBar
if (mPost != null) {
MenuItem saveMenuItem = menu.findItem(R.id.menu_save_post);
if (saveMenuItem != null) {
saveMenuItem.setTitle(getSaveButtonText());
}
}
return super.onPrepareOptionsMenu(menu);
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String permissions[],
@NonNull int[] grantResults) {
switch (requestCode) {
case LOCATION_PERMISSION_REQUEST_CODE:
boolean shouldShowLocation = false;
// Check if at least one of the location permission (coarse or fine) is granted
for (int grantResult : grantResults) {
if (grantResult == PackageManager.PERMISSION_GRANTED) {
shouldShowLocation = true;
}
}
if (shouldShowLocation) {
// Permission request was granted, show Location buttons in Settings
mEditPostSettingsFragment.showLocationSearch();
// After permission request was granted add GeoTag to the new post (if GeoTagging is enabled)
mEditPostSettingsFragment.searchLocation();
return;
}
// Location permission denied
ToastUtils.showToast(this, getString(R.string.add_location_permission_required));
break;
case MEDIA_PERMISSION_REQUEST_CODE:
boolean shouldShowContextMenu = true;
for (int i = 0; i < grantResults.length; ++i) {
switch (permissions[i]) {
case Manifest.permission.CAMERA:
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
shouldShowContextMenu = false;
}
break;
case Manifest.permission.WRITE_EXTERNAL_STORAGE:
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
shouldShowContextMenu = false;
} else {
registerReceiver(mGalleryReceiver,
new IntentFilter(LegacyEditorFragment.ACTION_MEDIA_GALLERY_TOUCHED));
refreshBlogMedia();
}
break;
}
}
if (shouldShowContextMenu) {
if (mMenuView != null) {
super.openContextMenu(mMenuView);
mMenuView = null;
}
} else {
ToastUtils.showToast(this, getString(R.string.access_media_permission_required));
}
break;
case PHOTO_PICKER_PERMISSION_REQUEST_CODE:
boolean canShowPhotoPicker = true;
for (int i = 0; i < grantResults.length; ++i) {
switch (permissions[i]) {
case Manifest.permission.CAMERA:
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
canShowPhotoPicker = false;
}
break;
case Manifest.permission.WRITE_EXTERNAL_STORAGE:
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
canShowPhotoPicker = false;
}
break;
}
}
if (canShowPhotoPicker) {
showPhotoPicker();
} else {
ToastUtils.showToast(this, getString(R.string.access_media_permission_required));
}
break;
case DRAG_AND_DROP_MEDIA_PERMISSION_REQUEST_CODE:
boolean mediaAccessGranted = false;
for (int i = 0; i < grantResults.length; ++i) {
switch (permissions[i]) {
case Manifest.permission.WRITE_EXTERNAL_STORAGE:
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
mediaAccessGranted = true;
}
break;
}
}
if (mediaAccessGranted) {
runOnUiThread(mFetchMediaRunnable);
} else {
ToastUtils.showToast(this, getString(R.string.access_media_permission_required));
}
default:
break;
}
}
// Menu actions
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
hidePhotoPicker();
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
Fragment fragment = getFragmentManager().findFragmentByTag(
ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG);
if (fragment != null && fragment.isVisible()) {
return false;
}
if (mViewPager.getCurrentItem() > PAGE_CONTENT) {
if (mViewPager.getCurrentItem() == PAGE_SETTINGS) {
mPost.setFeaturedImageId(mEditPostSettingsFragment.getFeaturedImageId());
mEditorFragment.setFeaturedImageId(mPost.getFeaturedImageId());
}
mViewPager.setCurrentItem(PAGE_CONTENT);
invalidateOptionsMenu();
} else {
savePostAndFinish();
}
return true;
}
// Disable format bar buttons while a media upload is in progress
if (mEditorFragment.isUploadingMedia() || mEditorFragment.isActionInProgress()) {
ToastUtils.showToast(this, R.string.editor_toast_uploading_please_wait, Duration.SHORT);
return false;
}
if (itemId == R.id.menu_save_post) {
publishPost();
} else if (itemId == R.id.menu_preview_post) {
mViewPager.setCurrentItem(PAGE_PREVIEW);
} else if (itemId == R.id.menu_post_settings) {
InputMethodManager imm = ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE));
imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
if (mShowNewEditor || mShowAztecEditor) {
mEditPostSettingsFragment.updateFeaturedImage(mPost.getFeaturedImageId());
}
mViewPager.setCurrentItem(PAGE_SETTINGS);
}
return false;
}
private void savePostOnlineAndFinishAsync(boolean isFirstTimePublish) {
new SavePostOnlineAndFinishTask(isFirstTimePublish).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void onUploadSuccess(MediaModel media) {
if (mEditorMediaUploadListener != null && media != null) {
mEditorMediaUploadListener.onMediaUploadSucceeded(String.valueOf(media.getId()),
FluxCUtils.mediaFileFromMediaModel(media));
}
removeMediaFromPendingList(media);
}
private void onUploadCanceled(MediaModel media) {
removeMediaFromPendingList(media);
}
private void onUploadError(MediaModel media, MediaStore.MediaError error) {
String localMediaId = String.valueOf(media.getId());
Map<String, Object> properties = null;
MediaFile mf = FluxCUtils.mediaFileFromMediaModel(media);
if (mf != null) {
properties = AnalyticsUtils.getMediaProperties(this, mf.isVideo(), null, mf.getFilePath());
properties.put("error_type", error.type.name());
}
AnalyticsTracker.track(Stat.EDITOR_UPLOAD_MEDIA_FAILED, properties);
// Display custom error depending on error type
String errorMessage;
switch (error.type) {
case AUTHORIZATION_REQUIRED:
errorMessage = getString(R.string.media_error_no_permission_upload);
break;
case REQUEST_TOO_LARGE:
errorMessage = getString(R.string.media_error_too_large_upload);
break;
case GENERIC_ERROR:
default:
errorMessage = TextUtils.isEmpty(error.message) ? getString(R.string.tap_to_try_again) : error.message;
}
if (mEditorMediaUploadListener != null) {
mEditorMediaUploadListener.onMediaUploadFailed(localMediaId, errorMessage);
}
removeMediaFromPendingList(media);
}
private void onUploadProgress(MediaModel media, float progress) {
String localMediaId = String.valueOf(media.getId());
if (mEditorMediaUploadListener != null) {
mEditorMediaUploadListener.onMediaUploadProgress(localMediaId, progress);
}
}
private void removeMediaFromPendingList(MediaModel mediaToClear) {
if (mediaToClear == null) {
return;
}
for (MediaModel pendingUpload : mPendingUploads) {
if (pendingUpload.getId() == mediaToClear.getId()) {
mPendingUploads.remove(pendingUpload);
break;
}
}
}
private void launchPictureLibrary() {
WordPressMediaUtils.launchPictureLibrary(this);
AppLockManager.getInstance().setExtendedTimeout();
}
private void launchVideoLibrary() {
WordPressMediaUtils.launchVideoLibrary(this);
AppLockManager.getInstance().setExtendedTimeout();
}
private void launchVideoCamera() {
WordPressMediaUtils.launchVideoCamera(this);
AppLockManager.getInstance().setExtendedTimeout();
}
private void showErrorAndFinish(int errorMessageId) {
Toast.makeText(this, getResources().getText(errorMessageId), Toast.LENGTH_LONG).show();
finish();
}
private void trackEditorCreatedPost(String action, Intent intent) {
Map<String, Object> properties = new HashMap<>();
// Post created from the post list (new post button).
String normalizedSourceName = "post-list";
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
// Post created with share with WordPress
normalizedSourceName = "shared-from-external-app";
}
if (EditPostActivity.NEW_MEDIA_POST.equals(
action)) {
// Post created from the media library
normalizedSourceName = "media-library";
}
if (intent != null && intent.hasExtra(EXTRA_IS_QUICKPRESS)) {
// Quick press
normalizedSourceName = "quick-press";
}
properties.put("created_post_source", normalizedSourceName);
AnalyticsUtils.trackWithSiteDetails(
AnalyticsTracker.Stat.EDITOR_CREATED_POST,
mSiteStore.getSiteByLocalId(mPost.getLocalSiteId()),
properties
);
}
private synchronized void updatePostObject(boolean isAutosave) throws IllegalEditorStateException {
if (mPost == null) {
AppLog.e(AppLog.T.POSTS, "Attempted to save an invalid Post.");
return;
}
// Update post object from fragment fields
if (mEditorFragment != null) {
if (mShowNewEditor || mShowAztecEditor) {
updatePostContentNewEditor(isAutosave, (String) mEditorFragment.getTitle(),
(String) mEditorFragment.getContent());
} else {
// TODO: Remove when legacy editor is dropped
updatePostContent(isAutosave);
}
}
if (mEditPostSettingsFragment != null) {
mEditPostSettingsFragment.updatePostSettings(mPost);
}
mPost.setDateLocallyChanged(DateTimeUtils.iso8601FromTimestamp(System.currentTimeMillis() / 1000));
}
private void savePostAsync(final AfterSavePostListener listener) {
new Thread(new Runnable() {
@Override
public void run() {
try {
updatePostObject(false);
} catch (IllegalEditorStateException e) {
AppLog.e(T.EDITOR, "Impossible to save the post, we weren't able to update it.");
return;
}
savePostToDb();
if (listener != null) {
listener.onPostSave();
}
}
}).start();
}
private interface AfterSavePostListener {
void onPostSave();
}
private synchronized void savePostToDb() {
mDispatcher.dispatch(PostActionBuilder.newUpdatePostAction(mPost));
// update the original post object, so we'll know of new changes
mOriginalPost = mPost.clone();
}
@Override
public void onBackPressed() {
if (isPhotoPickerShowing()) {
hidePhotoPicker();
return;
}
Fragment imageSettingsFragment = getFragmentManager().findFragmentByTag(
ImageSettingsDialogFragment.IMAGE_SETTINGS_DIALOG_TAG);
if (imageSettingsFragment != null && imageSettingsFragment.isVisible()) {
((ImageSettingsDialogFragment) imageSettingsFragment).dismissFragment();
return;
}
if (mViewPager.getCurrentItem() > PAGE_CONTENT) {
if (mViewPager.getCurrentItem() == PAGE_SETTINGS) {
mPost.setFeaturedImageId(mEditPostSettingsFragment.getFeaturedImageId());
mEditorFragment.setFeaturedImageId(mPost.getFeaturedImageId());
}
mViewPager.setCurrentItem(PAGE_CONTENT);
invalidateOptionsMenu();
return;
}
if (mEditorFragment != null && !mEditorFragment.onBackPressed()) {
savePostAndFinish();
}
}
public boolean isNewPost() {
return mIsNewPost;
}
private class SavePostOnlineAndFinishTask extends AsyncTask<Void, Void, Void> {
boolean isFirstTimePublish;
SavePostOnlineAndFinishTask(boolean isFirstTimePublish) {
this.isFirstTimePublish = isFirstTimePublish;
}
@Override
protected Void doInBackground(Void... params) {
PostUtils.trackSavePostAnalytics(mPost, mSiteStore.getSiteByLocalId(mPost.getLocalSiteId()));
if (isFirstTimePublish) {
PostUploadService.addPostToUploadAndTrackAnalytics(mPost);
} else {
PostUploadService.addPostToUpload(mPost);
}
PostUploadService.setLegacyMode(!mShowNewEditor && !mShowAztecEditor);
startService(new Intent(EditPostActivity.this, PostUploadService.class));
PendingDraftsNotificationsUtils.cancelPendingDraftAlarms(EditPostActivity.this, mPost.getId());
return null;
}
@Override
protected void onPostExecute(Void saved) {
saveResult(true, false);
finish();
}
}
private class SavePostLocallyAndFinishTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
if (mOriginalPost != null && !PostUtils.postHasEdits(mOriginalPost, mPost)) {
// If no changes have been made to the post, set it back to the original - don't save it
mDispatcher.dispatch(PostActionBuilder.newUpdatePostAction(mOriginalPost));
return false;
} else {
// Changes have been made - save the post and ask for the post list to refresh
// We consider this being "manual save", it will replace some Android "spans" by an html
// or a shortcode replacement (for instance for images and galleries)
if (mShowNewEditor || mShowAztecEditor) {
// Update the post object directly, without re-fetching the fields from the EditorFragment
updatePostContentNewEditor(false, mPost.getTitle(), mPost.getContent());
}
savePostToDb();
// now set the pending notification alarm to be triggered in the next day, week, and month
PendingDraftsNotificationsUtils.scheduleNextNotifications(EditPostActivity.this, mPost);
}
return true;
}
@Override
protected void onPostExecute(Boolean saved) {
saveResult(saved, true);
finish();
}
}
private void saveResult(boolean saved, boolean savedLocally) {
Intent i = getIntent();
i.putExtra(EXTRA_SAVED_AS_LOCAL_DRAFT, savedLocally);
i.putExtra(EXTRA_HAS_UNFINISHED_MEDIA, hasUnfinishedMedia());
i.putExtra(EXTRA_IS_PAGE, mIsPage);
i.putExtra(EXTRA_HAS_CHANGES, saved);
i.putExtra(EXTRA_POST, mPost);
setResult(RESULT_OK, i);
}
private void publishPost() {
AccountModel account = mAccountStore.getAccount();
// prompt user to verify e-mail before publishing
if (!account.getEmailVerified()) {
String message = TextUtils.isEmpty(account.getEmail())
? getString(R.string.editor_confirm_email_prompt_message)
: String.format(getString(R.string.editor_confirm_email_prompt_message_with_email), account.getEmail());
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.editor_confirm_email_prompt_title)
.setMessage(message)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
ToastUtils.showToast(EditPostActivity.this, getString(R.string.toast_saving_post_as_draft));
savePostAndFinish();
}
})
.setNegativeButton(R.string.editor_confirm_email_prompt_negative,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
mDispatcher.dispatch(AccountActionBuilder.newSendVerificationEmailAction());
}
});
builder.create().show();
return;
}
boolean isFirstTimePublish = isFirstTimePublish();
boolean postUpdateSuccessful = updatePostObject();
if (!postUpdateSuccessful) {
// just return, since the only case updatePostObject() can fail is when the editor
// fragment is not added to the activity
return;
}
// Update post, save to db and publish in its own Thread, because 1. update can be pretty slow with a lot of
// text 2. better not to call `updatePostObject()` from the UI thread due to weird thread blocking behavior
// on API 16 (and 21) with the visual editor.
new Thread(new Runnable() {
@Override
public void run() {
boolean isFirstTimePublish = isFirstTimePublish();
boolean postUpdateSuccessful = updatePostObject();
if (!postUpdateSuccessful) {
// just return, since the only case updatePostObject() can fail is when the editor
// fragment is not added to the activity
return;
}
boolean isPublishable = PostUtils.isPublishable(mPost);
// if post was modified or has unsaved local changes and is publishable, save it
saveResult(isPublishable, false);
if (isPublishable) {
if (NetworkUtils.isNetworkAvailable(getBaseContext())) {
// Show an Alert Dialog asking the user if they want to remove all failed media before upload
if (mEditorFragment.hasFailedMediaUploads()) {
EditPostActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
showRemoveFailedUploadsDialog();
}
});
} else {
savePostOnlineAndFinishAsync(isFirstTimePublish);
}
} else {
savePostLocallyAndFinishAsync();
}
} else {
EditPostActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtils.showToast(EditPostActivity.this, R.string.error_publish_empty_post, Duration.SHORT);
}
});
}
}
}).start();
}
private void showRemoveFailedUploadsDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.editor_toast_failed_uploads)
.setPositiveButton(R.string.editor_remove_failed_uploads, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// Clear failed uploads
mEditorFragment.removeAllFailedMediaUploads();
}
}).setNegativeButton(android.R.string.cancel, null);
builder.create().show();
}
private void savePostAndFinish() {
// Update post, save to db and post online in its own Thread, because 1. update can be pretty slow with a lot of
// text 2. better not to call `updatePostObject()` from the UI thread due to weird thread blocking behavior
// on API 16 (and 21) with the visual editor.
new Thread(new Runnable() {
@Override
public void run() {
// check if the opened post had some unsaved local changes
boolean hasLocalChanges = mPost.isLocallyChanged() || mPost.isLocalDraft();
boolean isFirstTimePublish = isFirstTimePublish();
boolean postUpdateSuccessful = updatePostObject();
if (!postUpdateSuccessful) {
// just return, since the only case updatePostObject() can fail is when the editor
// fragment is not added to the activity
return;
}
boolean hasChanges = PostUtils.postHasEdits(mOriginalPost, mPost);
boolean isPublishable = PostUtils.isPublishable(mPost);
boolean hasUnpublishedLocalDraftChanges = PostStatus.fromPost(mPost) == PostStatus.DRAFT &&
isPublishable && hasLocalChanges;
// if post was modified or has unsaved local changes and is publishable, save it
boolean shouldSave = (hasChanges || hasUnpublishedLocalDraftChanges) && (isPublishable || !isNewPost());
saveResult(shouldSave, false);
if (shouldSave) {
if (isNewPost()) {
// new post - user just left the editor without publishing, they probably want
// to keep the post as a draft
mPost.setStatus(PostStatus.DRAFT.toString());
if (mEditPostSettingsFragment != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mEditPostSettingsFragment.updateStatusSpinner();
}
});
}
}
if (PostStatus.fromPost(mPost) == PostStatus.DRAFT && isPublishable && !hasUnfinishedMedia()
&& NetworkUtils.isNetworkAvailable(getBaseContext())) {
savePostOnlineAndFinishAsync(isFirstTimePublish);
} else {
savePostLocallyAndFinishAsync();
}
} else {
// discard post if new & empty
if (!isPublishable && isNewPost()) {
mDispatcher.dispatch(PostActionBuilder.newRemovePostAction(mPost));
}
finish();
}
}
}).start();
}
private boolean isFirstTimePublish() {
return PostStatus.fromPost(mPost) == PostStatus.PUBLISHED &&
(mPost.isLocalDraft() || PostStatus.fromPost(mOriginalPost) == PostStatus.DRAFT);
}
private boolean hasUnfinishedMedia() {
return mEditorFragment.isUploadingMedia() || mEditorFragment.isActionInProgress() ||
mEditorFragment.hasFailedMediaUploads();
}
private boolean updatePostObject() {
try {
updatePostObject(false);
} catch (IllegalEditorStateException e) {
AppLog.e(T.EDITOR, "Impossible to save and publish the post, we weren't able to update it.");
return false;
}
return true;
}
private void savePostLocallyAndFinishAsync() {
new SavePostLocallyAndFinishTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Disable visual editor mode and log the exception if we get a Reflection failure when the webview is being
* initialized.
*/
@Override
public void onReflectionFailure(ReflectionException e) {
CrashlyticsUtils.logException(e, T.EDITOR, "Reflection Failure on Visual Editor init");
// Disable visual editor and show an error message
AppPrefs.setVisualEditorEnabled(false);
ToastUtils.showToast(this, R.string.new_editor_reflection_error, Duration.LONG);
// Restart the activity (will start the legacy editor)
finish();
startActivity(getIntent());
}
/**
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// Show two pages for the visual editor, and add a third page for the EditPostPreviewFragment for legacy
private static final int NUM_PAGES_VISUAL_EDITOR = 2;
private static final int NUM_PAGES_LEGACY_EDITOR = 3;
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
switch (position) {
case 0:
// TODO: Remove editor options after testing.
if (mShowAztecEditor) {
mAztecEditorFragment = AztecEditorFragment.newInstance("", "");
mAztecEditorFragment.setImageLoader(new AztecImageLoader(getBaseContext()));
return mAztecEditorFragment;
} else if (mShowNewEditor) {
EditorWebViewCompatibility.setReflectionFailureListener(EditPostActivity.this);
return new EditorFragment();
} else {
return new LegacyEditorFragment();
}
case 1:
return EditPostSettingsFragment.newInstance(mSite, mPost);
default:
return EditPostPreviewFragment.newInstance(mSite, mPost);
}
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
switch (position) {
case 0:
mEditorFragment = (EditorFragmentAbstract) fragment;
mEditorFragment.setImageLoader(mImageLoader);
if (mEditorFragment instanceof EditorMediaUploadListener) {
mEditorMediaUploadListener = (EditorMediaUploadListener) mEditorFragment;
// Set up custom headers for the visual editor's internal WebView
mEditorFragment.setCustomHttpHeader("User-Agent", WordPress.getUserAgent());
}
break;
case 1:
mEditPostSettingsFragment = (EditPostSettingsFragment) fragment;
break;
case 2:
mEditPostPreviewFragment = (EditPostPreviewFragment) fragment;
break;
}
return fragment;
}
@Override
public int getCount() {
return ((mShowNewEditor || mShowAztecEditor) ? NUM_PAGES_VISUAL_EDITOR : NUM_PAGES_LEGACY_EDITOR);
}
}
// Moved from EditPostContentFragment
public static final String NEW_MEDIA_POST = "NEW_MEDIA_POST";
public static final String NEW_MEDIA_POST_EXTRA_IDS = "NEW_MEDIA_POST_EXTRA_IDS";
private String mMediaCapturePath = "";
private int mMaxThumbWidth = 0;
private int getMaximumThumbnailWidthForEditor() {
if (mMaxThumbWidth == 0) {
mMaxThumbWidth = ImageUtils.getMaximumThumbnailWidthForEditor(this);
}
return mMaxThumbWidth;
}
private void addExistingMediaToEditor(long mediaId) {
MediaModel media = mMediaStore.getSiteMediaWithId(mSite, mediaId);
if (media != null) {
MediaFile mediaFile = FluxCUtils.mediaFileFromMediaModel(media);
trackAddMediaFromWPLibraryEvents(mediaFile.isVideo(), media.getMediaId());
String urlToUse = TextUtils.isEmpty(media.getUrl()) ? media.getFilePath() : media.getUrl();
mEditorFragment.appendMediaFile(mediaFile, urlToUse, mImageLoader);
}
}
private class LoadPostContentTask extends AsyncTask<String, Spanned, Spanned> {
@Override
protected Spanned doInBackground(String... params) {
if (params.length < 1 || mPost == null) {
return null;
}
String content = StringUtils.notNullStr(params[0]);
return WPHtml.fromHtml(content, EditPostActivity.this, mPost, getMaximumThumbnailWidthForEditor());
}
@Override
protected void onPostExecute(Spanned spanned) {
if (spanned != null) {
mEditorFragment.setContent(spanned);
}
}
}
private String getUploadErrorHtml(String mediaId, String path) {
String replacement;
if (Build.VERSION.SDK_INT >= 19) {
replacement = String.format(Locale.US,
"<span id=\"img_container_%s\" class=\"img_container failed\" data-failed=\"%s\"><progress " +
"id=\"progress_%s\" value=\"0\" class=\"wp_media_indicator failed\" contenteditable=\"false\">" +
"</progress><img data-wpid=\"%s\" src=\"%s\" alt=\"\" class=\"failed\"></span>",
mediaId, getString(R.string.tap_to_try_again), mediaId, mediaId, path);
} else {
// Before API 19, the WebView didn't support progress tags. Use an upload overlay instead of a progress bar
replacement = String.format(Locale.US,
"<span id=\"img_container_%s\" class=\"img_container compat failed\" contenteditable=\"false\" " +
"data-failed=\"%s\"><span class=\"upload-overlay failed\" " +
"contenteditable=\"false\">Uploading…</span><span class=\"upload-overlay-bg\"></span>" +
"<img data-wpid=\"%s\" src=\"%s\" alt=\"\" class=\"failed\"></span>",
mediaId, getString(R.string.tap_to_try_again), mediaId, path);
}
return replacement;
}
private String migrateLegacyDraft(String content) {
if (content.contains("<img src=\"null\" android-uri=\"")) {
// We must replace image tags specific to the legacy editor local drafts:
// <img src="null" android-uri="file:///..." />
// And trigger an upload action for the specific image / video
Pattern pattern = Pattern.compile("<img src=\"null\" android-uri=\"([^\"]*)\".*>");
Matcher matcher = pattern.matcher(content);
StringBuffer stringBuffer = new StringBuffer();
while (matcher.find()) {
String stringUri = matcher.group(1);
Uri uri = Uri.parse(stringUri);
MediaFile mediaFile = FluxCUtils.mediaFileFromMediaModel(queueFileForUpload(uri,
getContentResolver().getType(uri), UploadState.FAILED));
if (mediaFile == null) {
continue;
}
String replacement = getUploadErrorHtml(mediaFile.getMediaId(), mediaFile.getFilePath());
matcher.appendReplacement(stringBuffer, replacement);
}
matcher.appendTail(stringBuffer);
content = stringBuffer.toString();
}
if (content.contains("[caption")) {
// Convert old legacy post caption formatting to new format, to avoid being stripped by the visual editor
Pattern pattern = Pattern.compile("(\\[caption[^]]*caption=\"([^\"]*)\"[^]]*].+?)(\\[\\/caption])");
Matcher matcher = pattern.matcher(content);
StringBuffer stringBuffer = new StringBuffer();
while (matcher.find()) {
String replacement = matcher.group(1) + matcher.group(2) + matcher.group(3);
matcher.appendReplacement(stringBuffer, replacement);
}
matcher.appendTail(stringBuffer);
content = stringBuffer.toString();
}
return content;
}
private void fillContentEditorFields() {
// Needed blog settings needed by the editor
mEditorFragment.setFeaturedImageSupported(mSite.isFeaturedImageSupported());
// Set up the placeholder text
mEditorFragment.setContentPlaceholder(getString(R.string.editor_content_placeholder));
mEditorFragment.setTitlePlaceholder(getString(mIsPage ? R.string.editor_page_title_placeholder :
R.string.editor_post_title_placeholder));
// Set post title and content
if (mPost != null) {
if (!TextUtils.isEmpty(mPost.getContent()) && !mHasSetPostContent) {
mHasSetPostContent = true;
if (mPost.isLocalDraft() && !mShowNewEditor && !mShowAztecEditor) {
// TODO: Unnecessary for new editor, as all images are uploaded right away, even for local drafts
// Load local post content in the background, as it may take time to generate images
new LoadPostContentTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
mPost.getContent().replaceAll("\uFFFC", ""));
} else {
// TODO: Might be able to drop .replaceAll() when legacy editor is removed
String content = mPost.getContent().replaceAll("\uFFFC", "");
// Prepare eventual legacy editor local draft for the new editor
content = migrateLegacyDraft(content);
mEditorFragment.setContent(content);
}
}
if (!TextUtils.isEmpty(mPost.getTitle())) {
mEditorFragment.setTitle(mPost.getTitle());
}
// TODO: postSettingsButton.setText(post.isPage() ? R.string.page_settings : R.string.post_settings);
mEditorFragment.setLocalDraft(mPost.isLocalDraft());
mEditorFragment.setFeaturedImageId(mPost.getFeaturedImageId());
}
// Special actions
String action = getIntent().getAction();
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
setPostContentFromShareAction();
} else if (NEW_MEDIA_POST.equals(action)) {
prepareMediaPost();
}
}
private void launchCamera() {
WordPressMediaUtils.launchCamera(this, BuildConfig.APPLICATION_ID,
new WordPressMediaUtils.LaunchCameraCallback() {
@Override
public void onMediaCapturePathReady(String mediaCapturePath) {
mMediaCapturePath = mediaCapturePath;
AppLockManager.getInstance().setExtendedTimeout();
}
});
}
protected void setPostContentFromShareAction() {
Intent intent = getIntent();
// Check for shared text
String text = intent.getStringExtra(Intent.EXTRA_TEXT);
String title = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (text != null) {
if (title != null) {
mEditorFragment.setTitle(title);
}
// Create an <a href> element around links
text = AutolinkUtils.autoCreateLinks(text);
if (mEditorFragment instanceof LegacyEditorFragment) {
mEditorFragment.setContent(WPHtml.fromHtml(StringUtils.addPTags(text), this, mPost,
getMaximumThumbnailWidthForEditor()));
} else {
mEditorFragment.setContent(text);
}
}
// Check for shared media
if (intent.hasExtra(Intent.EXTRA_STREAM)) {
String action = intent.getAction();
String type = intent.getType();
ArrayList<Uri> sharedUris;
if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
sharedUris = intent.getParcelableArrayListExtra((Intent.EXTRA_STREAM));
} else {
// For a single media share, we only allow images and video types
if (type != null && (type.startsWith("image") || type.startsWith("video"))) {
sharedUris = new ArrayList<Uri>();
sharedUris.add((Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM));
} else {
return;
}
}
if (sharedUris != null) {
for (Uri uri : sharedUris) {
addMedia(uri);
}
}
}
}
private void startMediaGalleryActivity(MediaGallery mediaGallery) {
ActivityLauncher.viewMediaGalleryForSiteAndGallery(this, mSite, mediaGallery);
}
private void prepareMediaPost() {
long[] idsArray = getIntent().getLongArrayExtra(NEW_MEDIA_POST_EXTRA_IDS);
ArrayList<Long> idsList = ListUtils.fromLongArray(idsArray);
for (Long id: idsList) {
addExistingMediaToEditor(id);
}
}
// TODO: Replace with contents of the updatePostContentNewEditor() method when legacy editor is dropped
/**
* Updates post object with content of this fragment
*/
public void updatePostContent(boolean isAutoSave) throws IllegalEditorStateException {
if (mPost == null) {
return;
}
String title = StringUtils.notNullStr((String) mEditorFragment.getTitle());
SpannableStringBuilder postContent;
if (mEditorFragment.getSpannedContent() != null) {
// needed by the legacy editor to save local drafts
try {
postContent = new SpannableStringBuilder(mEditorFragment.getSpannedContent());
} catch (RuntimeException e) {
// A core android bug might cause an out of bounds exception, if so we'll just use the current editable
// See https://code.google.com/p/android/issues/detail?id=5164
postContent = new SpannableStringBuilder(StringUtils.notNullStr((String) mEditorFragment.getContent()));
}
} else {
postContent = new SpannableStringBuilder(StringUtils.notNullStr((String) mEditorFragment.getContent()));
}
String content;
if (mPost.isLocalDraft()) {
// remove suggestion spans, they cause craziness in WPHtml.toHTML().
CharacterStyle[] characterStyles = postContent.getSpans(0, postContent.length(), CharacterStyle.class);
for (CharacterStyle characterStyle : characterStyles) {
if (characterStyle instanceof SuggestionSpan) {
postContent.removeSpan(characterStyle);
}
}
content = WPHtml.toHtml(postContent);
// replace duplicate <p> tags so there's not duplicates, trac #86
content = content.replace("<p><p>", "<p>");
content = content.replace("</p></p>", "</p>");
content = content.replace("<br><br>", "<br>");
// sometimes the editor creates extra tags
content = content.replace("</strong><strong>", "").replace("</em><em>", "").replace("</u><u>", "")
.replace("</strike><strike>", "").replace("</blockquote><blockquote>", "");
} else {
if (!isAutoSave) {
// Add gallery shortcode
MediaGalleryImageSpan[] gallerySpans = postContent.getSpans(0, postContent.length(),
MediaGalleryImageSpan.class);
for (MediaGalleryImageSpan gallerySpan : gallerySpans) {
int start = postContent.getSpanStart(gallerySpan);
postContent.removeSpan(gallerySpan);
postContent.insert(start, WPHtml.getGalleryShortcode(gallerySpan));
}
}
WPImageSpan[] imageSpans = postContent.getSpans(0, postContent.length(), WPImageSpan.class);
if (imageSpans.length != 0) {
for (WPImageSpan wpIS : imageSpans) {
MediaFile mediaFile = wpIS.getMediaFile();
if (mediaFile == null) {
continue;
}
if (mediaFile.getMediaId() != null) {
updateMediaFileOnServer(mediaFile);
} else {
mediaFile.setFileName(wpIS.getImageSource().toString());
mediaFile.setFilePath(wpIS.getImageSource().toString());
}
int tagStart = postContent.getSpanStart(wpIS);
if (!isAutoSave) {
postContent.removeSpan(wpIS);
// network image has a mediaId
if (mediaFile.getMediaId() != null && mediaFile.getMediaId().length() > 0) {
postContent.insert(tagStart, WPHtml.getContent(wpIS));
} else {
// local image for upload
postContent.insert(tagStart,
"<img android-uri=\"" + wpIS.getImageSource().toString() + "\" />");
}
}
}
}
content = postContent.toString();
}
mPost.setTitle(title);
mPost.setContent(content);
if (!mPost.isLocalDraft()) {
mPost.setIsLocallyChanged(true);
}
}
/**
* Updates post object with given title and content
*/
public void updatePostContentNewEditor(boolean isAutoSave, String title, String content) {
if (mPost == null) {
return;
}
if (!isAutoSave) {
// TODO: Shortcode handling, media handling
}
mPost.setTitle(title);
mPost.setContent(content);
if (!mPost.isLocalDraft()) {
mPost.setIsLocallyChanged(true);
}
mPost.setDateLocallyChanged(DateTimeUtils.iso8601FromTimestamp(System.currentTimeMillis() / 1000));
}
private void updateMediaFileOnServer(MediaFile mediaFile) {
if (mediaFile == null) {
return;
}
MediaPayload payload = new MediaPayload(mSite, FluxCUtils.mediaModelFromMediaFile(mediaFile));
mDispatcher.dispatch(MediaActionBuilder.newPushMediaAction(payload));
}
/**
* Analytics about media from device
*
* @param isNew Whether is a fresh media
* @param isVideo Whether is a video or not
* @param uri The URI of the media on the device, or null
*/
private void trackAddMediaFromDeviceEvents(boolean isNew, boolean isVideo, Uri uri) {
if (uri == null) {
AppLog.e(T.MEDIA, "Cannot track new media events if both path and mediaURI are null!!");
return;
}
Map<String, Object> properties = AnalyticsUtils.getMediaProperties(this, isVideo, uri, null);
Stat currentStat;
if (isVideo) {
if (isNew) {
currentStat = Stat.EDITOR_ADDED_VIDEO_NEW;
} else {
currentStat = Stat.EDITOR_ADDED_VIDEO_VIA_DEVICE_LIBRARY;
}
} else {
if (isNew) {
currentStat = Stat.EDITOR_ADDED_PHOTO_NEW;
} else {
currentStat = Stat.EDITOR_ADDED_PHOTO_VIA_DEVICE_LIBRARY;
}
}
AnalyticsUtils.trackWithSiteDetails(currentStat, mSite, properties);
}
/**
* Analytics about media already available in the blog's library.
*
* @param isVideo Whether is a video or not
* @param mediaId The ID of the media in the WP blog's library, or null if device media.
*/
private void trackAddMediaFromWPLibraryEvents(boolean isVideo, long mediaId) {
if (mediaId == 0) {
AppLog.e(T.MEDIA, "Cannot track media events if mediaId is 0");
return;
}
if (isVideo) {
AnalyticsUtils.trackWithSiteDetails(Stat.EDITOR_ADDED_VIDEO_VIA_WP_MEDIA_LIBRARY, mSite, null);
} else {
AnalyticsUtils.trackWithSiteDetails(Stat.EDITOR_ADDED_PHOTO_VIA_WP_MEDIA_LIBRARY, mSite, null);
}
}
public boolean addMedia(Uri mediaUri) {
if (mediaUri != null && !MediaUtils.isInMediaStore(mediaUri) && !mediaUri.toString().startsWith("/")
&& !mediaUri.toString().startsWith("file://") ) {
mediaUri = MediaUtils.downloadExternalMedia(this, mediaUri);
}
if (mediaUri == null) {
return false;
}
boolean isVideo = MediaUtils.isVideo(mediaUri.toString());
if (mShowNewEditor || mShowAztecEditor) {
return addMediaVisualEditor(mediaUri, isVideo);
} else {
return addMediaLegacyEditor(mediaUri, isVideo);
}
}
private boolean addMediaVisualEditor(Uri uri, boolean isVideo) {
String path = MediaUtils.getRealPathFromURI(this, uri);
if (path == null) {
ToastUtils.showToast(this, R.string.file_not_found, Duration.SHORT);
return false;
}
Uri optimizedMedia = WPMediaUtils.getOptimizedMedia(this, mSite, path, isVideo);
if (optimizedMedia != null) {
uri = optimizedMedia;
path = MediaUtils.getRealPathFromURI(this, uri);
} else {
// Fix the rotation of the picture see https://github.com/wordpress-mobile/WordPress-Android/issues/5737
// TODO: find a better implementation
Uri rotatedMedia = WPMediaUtils.fixOrientationIssue(this, path, isVideo);
if (rotatedMedia != null) {
uri = rotatedMedia;
path = MediaUtils.getRealPathFromURI(this, uri);
}
}
MediaModel media = queueFileForUpload(uri, getContentResolver().getType(uri));
MediaFile mediaFile = FluxCUtils.mediaFileFromMediaModel(media);
if (media != null) {
mEditorFragment.appendMediaFile(mediaFile, path, mImageLoader);
}
return true;
}
private boolean addMediaLegacyEditor(Uri mediaUri, boolean isVideo) {
MediaModel mediaModel = buildMediaModel(mediaUri, getContentResolver().getType(mediaUri), UploadState.QUEUED);
if (isVideo) {
mediaModel.setTitle(getResources().getString(R.string.video));
} else {
mediaModel.setTitle(ImageUtils.getTitleForWPImageSpan(this, mediaUri.getEncodedPath()));
}
mediaModel.setLocalPostId(mPost.getId());
mDispatcher.dispatch(MediaActionBuilder.newUpdateMediaAction(mediaModel));
MediaFile mediaFile = FluxCUtils.mediaFileFromMediaModel(mediaModel);
mEditorFragment.appendMediaFile(mediaFile, mediaFile.getFilePath(), mImageLoader);
return true;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != Activity.RESULT_OK) {
return;
}
if (data != null || ((requestCode == RequestCodes.TAKE_PHOTO || requestCode == RequestCodes.TAKE_VIDEO))) {
switch (requestCode) {
case MediaGalleryActivity.REQUEST_CODE:
handleMediaGalleryResult(data);
// TODO: No analytics here?
break;
case MediaGalleryPickerActivity.REQUEST_CODE:
handleMediaGalleryPickerResult(data);
// No need to bump analytics here. Bumped later in handleMediaGalleryPickerResult-> addExistingMediaToEditor
break;
case RequestCodes.PICTURE_LIBRARY:
Uri imageUri = data.getData();
fetchMedia(imageUri);
trackAddMediaFromDeviceEvents(false, false, imageUri);
break;
case RequestCodes.TAKE_PHOTO:
try {
File f = new File(mMediaCapturePath);
Uri capturedImageUri = Uri.fromFile(f);
if (!addMedia(capturedImageUri)) {
ToastUtils.showToast(this, R.string.gallery_error, Duration.SHORT);
} else {
trackAddMediaFromDeviceEvents(true, false, capturedImageUri);
}
this.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"
+ Environment.getExternalStorageDirectory())));
} catch (RuntimeException e) {
AppLog.e(T.POSTS, e);
} catch (OutOfMemoryError e) {
AppLog.e(T.POSTS, e);
}
break;
case RequestCodes.VIDEO_LIBRARY:
Uri videoUri = data.getData();
List<Uri> mediaUris = Arrays.asList(videoUri);
for (Uri mediaUri : mediaUris) {
trackAddMediaFromDeviceEvents(false, true, mediaUri);
}
addAllMedia(mediaUris);
break;
case RequestCodes.TAKE_VIDEO:
Uri capturedVideoUri = MediaUtils.getLastRecordedVideoUri(this);
if (!addMedia(capturedVideoUri)) {
ToastUtils.showToast(this, R.string.gallery_error, Duration.SHORT);
} else {
AnalyticsTracker.track(Stat.EDITOR_ADDED_VIDEO_NEW);
trackAddMediaFromDeviceEvents(true, true, capturedVideoUri);
}
break;
}
}
}
private ArrayList<MediaModel> mPendingUploads = new ArrayList<>();
private void fetchMedia(Uri mediaUri) {
if (!MediaUtils.isInMediaStore(mediaUri)) {
// Do not download the file in async task. See https://github.com/wordpress-mobile/WordPress-Android/issues/5818
Uri downloadedUri = null;
try {
downloadedUri = MediaUtils.downloadExternalMedia(EditPostActivity.this, mediaUri);
} catch (IllegalStateException e) {
// Ref: https://github.com/wordpress-mobile/WordPress-Android/issues/5823
AppLog.e(AppLog.T.UTILS, "Can't download the image at: " + mediaUri.toString(), e);
CrashlyticsUtils.logException(e, AppLog.T.MEDIA, "Can't download the image at: " + mediaUri.toString() +
" See issue #5823");
}
if (downloadedUri != null) {
addMedia(downloadedUri);
} else {
Toast.makeText(EditPostActivity.this, getString(R.string.error_downloading_image),
Toast.LENGTH_SHORT).show();
}
} else {
addMedia(mediaUri);
}
}
/**
* Media
*/
private void addAllMedia(List<Uri> mediaUris) {
boolean isErrorAddingMedia = false;
for (Uri mediaUri : mediaUris) {
if (mediaUri == null || !addMedia(mediaUri)) {
isErrorAddingMedia = true;
}
}
if (isErrorAddingMedia) {
Toast.makeText(EditPostActivity.this, getResources().getText(R.string.gallery_error),
Toast.LENGTH_SHORT).show();
}
}
private String getRealPathFromURI(Uri uri) {
String path;
if ("content".equals(uri.getScheme())) {
path = getRealPathFromContentURI(uri);
} else if ("file".equals(uri.getScheme())) {
path = uri.getPath();
} else {
path = uri.toString();
}
return path;
}
private String getRealPathFromContentURI(Uri contentUri) {
if (contentUri == null)
return null;
String[] proj = { android.provider.MediaStore.Images.Media.DATA };
CursorLoader loader = new CursorLoader(this, contentUri, proj, null, null, null);
Cursor cursor = loader.loadInBackground();
if (cursor == null)
return null;
int column_index = cursor.getColumnIndex(proj[0]);
if (column_index == -1) {
cursor.close();
return null;
}
String path;
if (cursor.moveToFirst()) {
path = cursor.getString(column_index);
} else {
path = null;
}
cursor.close();
return path;
}
private void startMediaGalleryAddActivity() {
ActivityLauncher.viewMediaGalleryPickerForSite(this, mSite);
}
private void handleMediaGalleryPickerResult(Intent data) {
ArrayList<Long> ids = ListUtils.fromLongArray(data.getLongArrayExtra(MediaGalleryPickerActivity.RESULT_IDS));
if (ids == null || ids.size() == 0) {
return;
}
// if only one item was chosen insert it as a media object, otherwise show the insert
// media dialog so the user can choose how to insert the items
if (ids.size() == 1) {
long mediaId = ids.get(0);
addExistingMediaToEditor(mediaId);
} else {
showInsertMediaDialog(ids);
}
}
/*
* called after user selects multiple photos from WP media library
*/
private void showInsertMediaDialog(final ArrayList<Long> mediaIds) {
InsertMediaCallback callback = new InsertMediaCallback() {
@Override
public void onCompleted(@NonNull InsertMediaDialog dialog) {
switch (dialog.getInsertType()) {
case GALLERY:
MediaGallery gallery = new MediaGallery();
gallery.setType(dialog.getGalleryType().toString());
gallery.setNumColumns(dialog.getNumColumns());
gallery.setIds(mediaIds);
mEditorFragment.appendGallery(gallery);
break;
case INDIVIDUALLY:
for (Long id: mediaIds) {
addExistingMediaToEditor(id);
}
break;
}
}
};
InsertMediaDialog dialog = InsertMediaDialog.newInstance(callback);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(dialog, "insert_media");
ft.commitAllowingStateLoss();
}
private void handleMediaGalleryResult(Intent data) {
MediaGallery gallery = (MediaGallery) data.getSerializableExtra(MediaGalleryActivity.RESULT_MEDIA_GALLERY);
// if blank gallery returned, don't add to span
if (gallery == null || gallery.getIds().size() == 0) {
return;
}
mEditorFragment.appendGallery(gallery);
}
private BroadcastReceiver mGalleryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (LegacyEditorFragment.ACTION_MEDIA_GALLERY_TOUCHED.equals(intent.getAction())) {
startMediaGalleryActivity((MediaGallery)intent.getSerializableExtra(LegacyEditorFragment.EXTRA_MEDIA_GALLERY));
}
}
};
private void refreshBlogMedia() {
if (NetworkUtils.isNetworkAvailable(this)) {
FetchMediaListPayload payload = new FetchMediaListPayload(mSite, false);
mDispatcher.dispatch(MediaActionBuilder.newFetchMediaListAction(payload));
} else {
ToastUtils.showToast(this, R.string.error_media_refresh_no_connection, ToastUtils.Duration.SHORT);
}
}
@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onAccountChanged(OnAccountChanged event) {
if (event.causeOfChange == AccountAction.SEND_VERIFICATION_EMAIL) {
if (!event.isError()) {
ToastUtils.showToast(this, getString(R.string.toast_verification_email_sent));
} else {
ToastUtils.showToast(this, getString(R.string.toast_verification_email_send_error));
}
}
}
@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMediaChanged(OnMediaChanged event) {
if (event.isError()) {
final String errorMessage;
switch (event.error.type) {
case FS_READ_PERMISSION_DENIED:
errorMessage = getString(R.string.error_media_insufficient_fs_permissions);
break;
case NOT_FOUND:
errorMessage = getString(R.string.error_media_not_found);
break;
case AUTHORIZATION_REQUIRED:
errorMessage = getString(R.string.error_media_unauthorized);
break;
case PARSE_ERROR:
errorMessage = getString(R.string.error_media_parse_error);
break;
case MALFORMED_MEDIA_ARG:
case NULL_MEDIA_ARG:
case GENERIC_ERROR:
default:
errorMessage = getString(R.string.error_refresh_media);
break;
}
if (!TextUtils.isEmpty(errorMessage)) {
ToastUtils.showToast(EditPostActivity.this, errorMessage, ToastUtils.Duration.SHORT);
}
} else {
if (mPendingVideoPressInfoRequests != null && !mPendingVideoPressInfoRequests.isEmpty()) {
// If there are pending requests for video URLs from VideoPress ids, query the DB for
// them again and notify the editor
for (String videoId : mPendingVideoPressInfoRequests) {
String videoUrl = mMediaStore.
getUrlForSiteVideoWithVideoPressGuid(mSite, videoId);
String posterUrl = WordPressMediaUtils.getVideoPressVideoPosterFromURL(videoUrl);
mEditorFragment.setUrlForVideoPressId(videoId, videoUrl, posterUrl);
}
mPendingVideoPressInfoRequests.clear();
}
}
}
/**
* Starts the upload service to upload selected media.
*/
private void startMediaUploadService() {
if (mPendingUploads != null && !mPendingUploads.isEmpty()) {
ArrayList<MediaModel> mediaList = new ArrayList<>();
for (MediaModel media : mPendingUploads) {
if (UploadState.QUEUED.name().equals(media.getUploadState())) {
mediaList.add(media);
}
}
MediaUploadService.startService(this, mSite, mediaList);
}
}
private String getVideoThumbnail(String videoPath) {
String thumbnailPath = null;
try {
File outputFile = File.createTempFile("thumb", ".png", getCacheDir());
FileOutputStream outputStream = new FileOutputStream(outputFile);
Bitmap thumb = ThumbnailUtils.createVideoThumbnail(videoPath,
android.provider.MediaStore.Images.Thumbnails.MINI_KIND);
if (thumb != null) {
thumb.compress(Bitmap.CompressFormat.PNG, 75, outputStream);
thumbnailPath = outputFile.getAbsolutePath();
}
} catch (IOException e) {
AppLog.i(T.MEDIA, "Can't create thumbnail for video: " + videoPath);
}
return thumbnailPath;
}
/**
* Queues a media file for upload and starts the MediaUploadService. Toasts will alert the user
* if there are issues with the file.
*/
private MediaModel queueFileForUpload(Uri uri, String mimeType) {
return queueFileForUpload(uri, mimeType, UploadState.QUEUED);
}
private MediaModel queueFileForUpload(Uri uri, String mimeType, UploadState startingState) {
String path = getRealPathFromURI(uri);
// Invalid file path
if (TextUtils.isEmpty(path)) {
Toast.makeText(this, R.string.editor_toast_invalid_path, Toast.LENGTH_SHORT).show();
return null;
}
// File not found
File file = new File(path);
if (!file.exists()) {
Toast.makeText(this, R.string.file_not_found, Toast.LENGTH_SHORT).show();
return null;
}
MediaModel media = buildMediaModel(uri, mimeType, startingState);
mDispatcher.dispatch(MediaActionBuilder.newUpdateMediaAction(media));
mPendingUploads.add(media);
startMediaUploadService();
return media;
}
private MediaModel buildMediaModel(Uri uri, String mimeType, UploadState startingState) {
String path = getRealPathFromURI(uri);
MediaModel media = mMediaStore.instantiateMediaModel();
AppLog.i(T.MEDIA, "New media instantiated localId=" + media.getId());
String filename = org.wordpress.android.fluxc.utils.MediaUtils.getFileName(path);
String fileExtension = org.wordpress.android.fluxc.utils.MediaUtils.getExtension(path);
// Try to get mimetype if none was passed to this method
if (mimeType == null) {
mimeType = getContentResolver().getType(uri);
if (mimeType == null) {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
}
if (mimeType == null) {
// Default to image jpeg
mimeType = "image/jpeg";
}
}
// If file extension is null, upload won't work on wordpress.com
if (fileExtension == null) {
fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
filename += "." + fileExtension;
}
if (org.wordpress.android.fluxc.utils.MediaUtils.isVideoMimeType(mimeType)) {
media.setThumbnailUrl(getVideoThumbnail(path));
}
media.setFileName(filename);
media.setFilePath(path);
media.setLocalSiteId(mSite.getId());
media.setFileExtension(fileExtension);
media.setMimeType(mimeType);
media.setUploadState(startingState.name());
media.setUploadDate(DateTimeUtils.iso8601UTCFromTimestamp(System.currentTimeMillis() / 1000));
if (!mPost.isLocalDraft()) {
media.setPostId(mPost.getRemotePostId());
}
return media;
}
/**
* EditorFragmentListener methods
*/
@Override
public void onSettingsClicked() {
mViewPager.setCurrentItem(PAGE_SETTINGS);
}
@Override
public void onAddMediaClicked() {
if (!isPhotoPickerShowing()) {
showPhotoPicker();
} else {
hidePhotoPicker();
}
}
@Override
public void onMediaDropped(final ArrayList<Uri> mediaUris) {
mDroppedMediaUris = mediaUris;
if (PermissionUtils.checkAndRequestStoragePermission(this, DRAG_AND_DROP_MEDIA_PERMISSION_REQUEST_CODE)) {
runOnUiThread(mFetchMediaRunnable);
}
}
@Override
public void onRequestDragAndDropPermissions(DragEvent dragEvent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
requestTemporaryPermissions(dragEvent);
}
}
@TargetApi(Build.VERSION_CODES.N)
private void requestTemporaryPermissions(DragEvent dragEvent) {
requestDragAndDropPermissions(dragEvent);
}
@Override
public void onMediaRetryClicked(String mediaId) {
if (TextUtils.isEmpty(mediaId)) {
AppLog.e(T.MEDIA, "Invalid media id passed to onMediaRetryClicked");
return;
}
MediaModel media = mMediaStore.getMediaWithLocalId(Integer.valueOf(mediaId));
if (media == null) {
AppLog.e(T.MEDIA, "Can't find media with local id: " + mediaId);
return;
}
if (UploadState.valueOf(media.getUploadState()) == UploadState.UPLOADED) {
// Note: we should actually do this when the editor fragment starts instead of waiting for user input.
// Notify the editor fragment upload was successful and it should replace the local url by the remote url.
if (mEditorMediaUploadListener != null) {
mEditorMediaUploadListener.onMediaUploadSucceeded(String.valueOf(media.getId()),
FluxCUtils.mediaFileFromMediaModel(media));
}
} else {
media.setUploadState(UploadState.QUEUED.name());
mDispatcher.dispatch(MediaActionBuilder.newUpdateMediaAction(media));
mPendingUploads.add(media);
startMediaUploadService();
}
AnalyticsTracker.track(Stat.EDITOR_UPLOAD_MEDIA_RETRIED);
}
@Override
public void onMediaUploadCancelClicked(String mediaId, boolean delete) {
if (!TextUtils.isEmpty(mediaId)) {
int localMediaId = Integer.valueOf(mediaId);
EventBus.getDefault().post(new PostEvents.PostMediaCanceled(localMediaId));
} else {
// Passed mediaId is incorrect: cancel all uploads
ToastUtils.showToast(this, getString(R.string.error_all_media_upload_canceled));
EventBus.getDefault().post(new PostEvents.PostMediaCanceled(true));
}
}
@Override
public void onFeaturedImageChanged(long mediaId) {
mPost.setFeaturedImageId(mediaId);
mEditPostSettingsFragment.updateFeaturedImage(mediaId);
}
@Override
public void onVideoPressInfoRequested(final String videoId) {
String videoUrl = mMediaStore.
getUrlForSiteVideoWithVideoPressGuid(mSite, videoId);
if (videoUrl.isEmpty()) {
if (PermissionUtils.checkAndRequestCameraAndStoragePermissions(this, MEDIA_PERMISSION_REQUEST_CODE)) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mPendingVideoPressInfoRequests == null) {
mPendingVideoPressInfoRequests = new ArrayList<>();
}
mPendingVideoPressInfoRequests.add(videoId);
refreshBlogMedia();
}
});
} else {
AppLockManager.getInstance().setExtendedTimeout();
}
}
String posterUrl = WordPressMediaUtils.getVideoPressVideoPosterFromURL(videoUrl);
mEditorFragment.setUrlForVideoPressId(videoId, videoUrl, posterUrl);
}
@Override
public String onAuthHeaderRequested(String url) {
String authHeader = "";
String token = mAccountStore.getAccessToken();
if (mSite.isPrivate() && WPUrlUtils.safeToAddWordPressComAuthToken(url)
&& !TextUtils.isEmpty(token)) {
authHeader = "Bearer " + token;
}
return authHeader;
}
@Override
public void onEditorFragmentInitialized() {
fillContentEditorFields();
// Set the error listener
if (mEditorFragment instanceof EditorFragment) {
mEditorFragment.setDebugModeEnabled(BuildConfig.DEBUG);
((EditorFragment) mEditorFragment).setWebViewErrorListener(new ErrorListener() {
@Override
public void onJavaScriptError(String sourceFile, int lineNumber, String message) {
CrashlyticsUtils.logException(new JavaScriptException(sourceFile, lineNumber, message),
T.EDITOR,
String.format(Locale.US, "%s:%d: %s", sourceFile, lineNumber, message));
}
@Override
public void onJavaScriptAlert(String url, String message) {
// no op
}
});
}
}
@Override
public void saveMediaFile(MediaFile mediaFile) {
}
@Override
public void onTrackableEvent(TrackableEvent event) {
switch (event) {
case HTML_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_HTML);
hidePhotoPicker();
break;
case MEDIA_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_IMAGE);
break;
case UNLINK_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNLINK);
break;
case LINK_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_LINK);
hidePhotoPicker();
break;
case IMAGE_EDITED:
AnalyticsTracker.track(Stat.EDITOR_EDITED_IMAGE);
break;
case BOLD_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_BOLD);
break;
case ITALIC_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_ITALIC);
break;
case OL_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_ORDERED_LIST);
break;
case UL_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNORDERED_LIST);
break;
case BLOCKQUOTE_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_BLOCKQUOTE);
break;
case STRIKETHROUGH_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_STRIKETHROUGH);
break;
case UNDERLINE_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_UNDERLINE);
break;
case MORE_BUTTON_TAPPED:
AnalyticsTracker.track(Stat.EDITOR_TAPPED_MORE);
break;
}
}
// FluxC events
@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMediaUploaded(MediaStore.OnMediaUploaded event) {
if (isFinishing()) return;
// event for unknown media, ignoring
if (event.media == null) {
AppLog.w(AppLog.T.MEDIA, "Media event not recognized: " + event.media);
return;
}
if (event.isError()) {
onUploadError(event.media, event.error);
}
else
if (event.canceled) {
onUploadCanceled(event.media);
}
else
if (event.completed) {
onUploadSuccess(event.media);
}
else {
onUploadProgress(event.media, event.progress);
}
}
}