aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUploadService.java
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUploadService.java')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUploadService.java1026
1 files changed, 1026 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUploadService.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUploadService.java
new file mode 100644
index 000000000..9b62c84b2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/PostUploadService.java
@@ -0,0 +1,1026 @@
+package org.wordpress.android.ui.posts.services;
+
+import android.app.Notification;
+import android.app.Notification.Builder;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.ThumbnailUtils;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Post;
+import org.wordpress.android.models.PostLocation;
+import org.wordpress.android.models.PostStatus;
+import org.wordpress.android.ui.notifications.ShareAndDismissNotificationReceiver;
+import org.wordpress.android.ui.posts.PostsListActivity;
+import org.wordpress.android.ui.posts.services.PostEvents.PostUploadEnded;
+import org.wordpress.android.ui.posts.services.PostEvents.PostUploadStarted;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.CrashlyticsUtils;
+import org.wordpress.android.util.DisplayUtils;
+import org.wordpress.android.util.ImageUtils;
+import org.wordpress.android.util.MediaUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.SystemServiceFactory;
+import org.wordpress.android.util.WPMeShortlinks;
+import org.wordpress.android.util.helpers.MediaFile;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCClient;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.greenrobot.event.EventBus;
+
+public class PostUploadService extends Service {
+ private static Context mContext;
+ private static final ArrayList<Post> mPostsList = new ArrayList<Post>();
+ private static Post mCurrentUploadingPost = null;
+ private static boolean mUseLegacyMode;
+ private UploadPostTask mCurrentTask = null;
+
+ public static void addPostToUpload(Post currentPost) {
+ synchronized (mPostsList) {
+ mPostsList.add(currentPost);
+ }
+ }
+
+ public static void setLegacyMode(boolean enabled) {
+ mUseLegacyMode = enabled;
+ }
+
+ /*
+ * returns true if the passed post is either uploading or waiting to be uploaded
+ */
+ public static boolean isPostUploading(long localPostId) {
+ // first check the currently uploading post
+ if (mCurrentUploadingPost != null && mCurrentUploadingPost.getLocalTablePostId() == localPostId) {
+ return true;
+ }
+ // then check the list of posts waiting to be uploaded
+ if (mPostsList.size() > 0) {
+ synchronized (mPostsList) {
+ for (Post post : mPostsList) {
+ if (post.getLocalTablePostId() == localPostId) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mContext = this.getApplicationContext();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // Cancel current task, it will reset post from "uploading" to "local draft"
+ if (mCurrentTask != null) {
+ AppLog.d(T.POSTS, "cancelling current upload task");
+ mCurrentTask.cancel(true);
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ synchronized (mPostsList) {
+ if (mPostsList.size() == 0 || mContext == null) {
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+ }
+
+ uploadNextPost();
+ // We want this service to continue running until it is explicitly stopped, so return sticky.
+ return START_STICKY;
+ }
+
+ private void uploadNextPost() {
+ synchronized (mPostsList) {
+ if (mCurrentTask == null) { //make sure nothing is running
+ mCurrentUploadingPost = null;
+ if (mPostsList.size() > 0) {
+ mCurrentUploadingPost = mPostsList.remove(0);
+ mCurrentTask = new UploadPostTask();
+ mCurrentTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, mCurrentUploadingPost);
+ } else {
+ stopSelf();
+ }
+ }
+ }
+ }
+
+ private void postUploaded() {
+ synchronized (mPostsList) {
+ mCurrentTask = null;
+ mCurrentUploadingPost = null;
+ }
+ uploadNextPost();
+ }
+
+ private class UploadPostTask extends AsyncTask<Post, Boolean, Boolean> {
+ private Post mPost;
+ private Blog mBlog;
+ private PostUploadNotifier mPostUploadNotifier;
+
+ private String mErrorMessage = "";
+ private boolean mIsMediaError = false;
+ private int featuredImageID = -1;
+ private XMLRPCClientInterface mClient;
+
+ // True when the post goes from draft or local draft to published status
+ boolean mIsFirstPublishing = false;
+
+ // Used when the upload succeed
+ private Bitmap mLatestIcon;
+
+ // Used for analytics
+ private boolean mHasImage, mHasVideo, mHasCategory;
+
+ @Override
+ protected void onPostExecute(Boolean postUploadedSuccessfully) {
+ if (postUploadedSuccessfully) {
+ WordPress.wpDB.deleteMediaFilesForPost(mPost);
+ mPostUploadNotifier.cancelNotification();
+ mPostUploadNotifier.updateNotificationSuccess(mPost, mLatestIcon, mIsFirstPublishing);
+ } else {
+ mPostUploadNotifier.updateNotificationError(mErrorMessage, mIsMediaError, mPost.isPage());
+ }
+
+ postUploaded();
+ EventBus.getDefault().post(new PostUploadEnded(postUploadedSuccessfully, mPost.getLocalTableBlogId()));
+ }
+
+ @Override
+ protected void onCancelled(Boolean aBoolean) {
+ super.onCancelled(aBoolean);
+ // mPostUploadNotifier and mPost can be null if onCancelled is called before doInBackground
+ if (mPostUploadNotifier != null && mPost != null) {
+ mPostUploadNotifier.updateNotificationError(mErrorMessage, mIsMediaError, mPost.isPage());
+ }
+ }
+
+ @Override
+ protected Boolean doInBackground(Post... posts) {
+ mPost = posts[0];
+
+ String postTitle = TextUtils.isEmpty(mPost.getTitle()) ? getString(R.string.untitled) : mPost.getTitle();
+ String uploadingPostTitle = String.format(getString(R.string.posting_post), postTitle);
+ String uploadingPostMessage = String.format(
+ getString(R.string.sending_content),
+ mPost.isPage() ? getString(R.string.page).toLowerCase() : getString(R.string.post).toLowerCase()
+ );
+ mPostUploadNotifier = new PostUploadNotifier(mPost, uploadingPostTitle, uploadingPostMessage);
+
+ mBlog = WordPress.wpDB.instantiateBlogByLocalId(mPost.getLocalTableBlogId());
+ if (mBlog == null) {
+ mErrorMessage = mContext.getString(R.string.blog_not_found);
+ return false;
+ }
+
+ // Create the XML-RPC client
+ mClient = XMLRPCFactory.instantiate(mBlog.getUri(), mBlog.getHttpuser(),
+ mBlog.getHttppassword());
+
+ if (TextUtils.isEmpty(mPost.getPostStatus())) {
+ mPost.setPostStatus(PostStatus.toString(PostStatus.PUBLISHED));
+ }
+
+ String descriptionContent = processPostMedia(mPost.getDescription());
+
+ String moreContent = "";
+ if (!TextUtils.isEmpty(mPost.getMoreText())) {
+ moreContent = processPostMedia(mPost.getMoreText());
+ }
+
+ // If media file upload failed, let's stop here and prompt the user
+ if (mIsMediaError) {
+ return false;
+ }
+
+ JSONArray categoriesJsonArray = mPost.getJSONCategories();
+ String[] postCategories = null;
+ if (categoriesJsonArray != null) {
+ if (categoriesJsonArray.length() > 0) {
+ mHasCategory = true;
+ }
+
+ postCategories = new String[categoriesJsonArray.length()];
+ for (int i = 0; i < categoriesJsonArray.length(); i++) {
+ try {
+ postCategories[i] = TextUtils.htmlEncode(categoriesJsonArray.getString(i));
+ } catch (JSONException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+ }
+
+ Map<String, Object> contentStruct = new HashMap<String, Object>();
+
+ // Post format
+ if (!mPost.isPage()) {
+ if (!TextUtils.isEmpty(mPost.getPostFormat())) {
+ contentStruct.put("wp_post_format", mPost.getPostFormat());
+ }
+ }
+
+ contentStruct.put("post_type", (mPost.isPage()) ? "page" : "post");
+ contentStruct.put("title", mPost.getTitle());
+ long pubDate = mPost.getDate_created_gmt();
+ if (pubDate != 0) {
+ Date date_created_gmt = new Date(pubDate);
+ contentStruct.put("date_created_gmt", date_created_gmt);
+ Date dateCreated = new Date(pubDate + (date_created_gmt.getTimezoneOffset() * 60000));
+ contentStruct.put("dateCreated", dateCreated);
+ }
+
+ if (!TextUtils.isEmpty(moreContent)) {
+ descriptionContent = descriptionContent.trim() + "<!--more-->" + moreContent;
+ mPost.setMoreText("");
+ }
+
+ // get rid of the p and br tags that the editor adds.
+ if (mPost.isLocalDraft()) {
+ descriptionContent = descriptionContent.replace("<p>", "").replace("</p>", "\n").replace("<br>", "");
+ }
+
+ // gets rid of the weird character android inserts after images
+ descriptionContent = descriptionContent.replaceAll("\uFFFC", "");
+
+ contentStruct.put("description", descriptionContent);
+ if (!mPost.isPage()) {
+ contentStruct.put("mt_keywords", mPost.getKeywords());
+
+ if (postCategories != null && postCategories.length > 0) {
+ contentStruct.put("categories", postCategories);
+ }
+ }
+
+ contentStruct.put("mt_excerpt", mPost.getPostExcerpt());
+ contentStruct.put((mPost.isPage()) ? "page_status" : "post_status", mPost.getPostStatus());
+
+ // Geolocation
+ if (mPost.supportsLocation()) {
+ JSONObject remoteGeoLatitude = mPost.getCustomField("geo_latitude");
+ JSONObject remoteGeoLongitude = mPost.getCustomField("geo_longitude");
+ JSONObject remoteGeoPublic = mPost.getCustomField("geo_public");
+
+ Map<Object, Object> hLatitude = new HashMap<Object, Object>();
+ Map<Object, Object> hLongitude = new HashMap<Object, Object>();
+ Map<Object, Object> hPublic = new HashMap<Object, Object>();
+
+ try {
+ if (remoteGeoLatitude != null) {
+ hLatitude.put("id", remoteGeoLatitude.getInt("id"));
+ }
+
+ if (remoteGeoLongitude != null) {
+ hLongitude.put("id", remoteGeoLongitude.getInt("id"));
+ }
+
+ if (remoteGeoPublic != null) {
+ hPublic.put("id", remoteGeoPublic.getInt("id"));
+ }
+
+ if (mPost.hasLocation()) {
+ PostLocation location = mPost.getLocation();
+ hLatitude.put("key", "geo_latitude");
+ hLongitude.put("key", "geo_longitude");
+ hPublic.put("key", "geo_public");
+ hLatitude.put("value", location.getLatitude());
+ hLongitude.put("value", location.getLongitude());
+ hPublic.put("value", 1);
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.EDITOR, e);
+ }
+
+ if (!hLatitude.isEmpty() && !hLongitude.isEmpty() && !hPublic.isEmpty()) {
+ Object[] geo = {hLatitude, hLongitude, hPublic};
+ contentStruct.put("custom_fields", geo);
+ }
+ }
+
+ // Featured images
+ if (mUseLegacyMode) {
+ // Support for legacy editor - images are identified as featured as they're being uploaded with the post
+ if (featuredImageID != -1) {
+ contentStruct.put("wp_post_thumbnail", featuredImageID);
+ }
+ } else if (mPost.featuredImageHasChanged()) {
+ if (mPost.getFeaturedImageId() < 1 && !mPost.isLocalDraft()) {
+ // The featured image was removed from a live post
+ contentStruct.put("wp_post_thumbnail", "");
+ } else {
+ contentStruct.put("wp_post_thumbnail", mPost.getFeaturedImageId());
+ }
+ }
+
+ if (!TextUtils.isEmpty(mPost.getQuickPostType())) {
+ mClient.addQuickPostHeader(mPost.getQuickPostType());
+ }
+
+ contentStruct.put("wp_password", mPost.getPassword());
+
+ Object[] params;
+ if (mPost.isLocalDraft())
+ params = new Object[]{mBlog.getRemoteBlogId(), mBlog.getUsername(), mBlog.getPassword(),
+ contentStruct, false};
+ else
+ params = new Object[]{mPost.getRemotePostId(), mBlog.getUsername(), mBlog.getPassword(), contentStruct,
+ false};
+
+ try {
+ EventBus.getDefault().post(new PostUploadStarted(mPost.getLocalTableBlogId()));
+
+ if (mPost.isLocalDraft()) {
+ Object object = mClient.call("metaWeblog.newPost", params);
+ if (object instanceof String) {
+ mPost.setRemotePostId((String) object);
+ }
+ } else {
+ mClient.call("metaWeblog.editPost", params);
+ }
+
+ // Check if it's the first publishing before changing post status.
+ mIsFirstPublishing = mPost.hasChangedFromDraftToPublished()
+ || (mPost.isLocalDraft() && mPost.getStatusEnum() == PostStatus.PUBLISHED);
+
+ mPost.setLocalDraft(false);
+ mPost.setLocalChange(false);
+ WordPress.wpDB.updatePost(mPost);
+
+ // Track analytics only if the post is newly published
+ if (mIsFirstPublishing) {
+ trackUploadAnalytics();
+ }
+
+ // request the new/updated post from the server to ensure local copy matches server
+ ApiHelper.updateSinglePost(mBlog.getLocalTableBlogId(), mPost.getRemotePostId(), mPost.isPage());
+
+ return true;
+ } catch (final XMLRPCException e) {
+ setUploadPostErrorMessage(e);
+ } catch (IOException e) {
+ setUploadPostErrorMessage(e);
+ } catch (XmlPullParserException e) {
+ setUploadPostErrorMessage(e);
+ }
+
+ return false;
+ }
+
+ private boolean hasGallery() {
+ Pattern galleryTester = Pattern.compile("\\[.*?gallery.*?\\]");
+ Matcher matcher = galleryTester.matcher(mPost.getContent());
+ return matcher.find();
+ }
+
+ private void trackUploadAnalytics() {
+ // Calculate the words count
+ Map<String, Object> properties = new HashMap<String, Object>();
+ properties.put("word_count", AnalyticsUtils.getWordCount(mPost.getContent()));
+
+ if (hasGallery()) {
+ properties.put("with_galleries", true);
+ }
+ if (mHasImage) {
+ properties.put("with_photos", true);
+ }
+ if (mHasVideo) {
+ properties.put("with_videos", true);
+ }
+ if (mHasCategory) {
+ properties.put("with_categories", true);
+ }
+ if (!TextUtils.isEmpty(mPost.getKeywords())) {
+ properties.put("with_tags", true);
+ }
+ properties.put("via_new_editor", AppPrefs.isVisualEditorEnabled());
+ AnalyticsUtils.trackWithBlogDetails(Stat.EDITOR_PUBLISHED_POST, mBlog, properties);
+ }
+
+ /**
+ * Finds media in post content, uploads them, and returns the HTML to insert in the post
+ */
+ private String processPostMedia(String postContent) {
+ String imageTagsPattern = "<img[^>]+android-uri\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>";
+ Pattern pattern = Pattern.compile(imageTagsPattern);
+ Matcher matcher = pattern.matcher(postContent);
+
+ int totalMediaItems = 0;
+ List<String> imageTags = new ArrayList<String>();
+ while (matcher.find()) {
+ imageTags.add(matcher.group());
+ totalMediaItems++;
+ }
+
+ mPostUploadNotifier.setTotalMediaItems(totalMediaItems);
+
+ int mediaItemCount = 0;
+ for (String tag : imageTags) {
+ Pattern p = Pattern.compile("android-uri=\"([^\"]+)\"");
+ Matcher m = p.matcher(tag);
+ if (m.find()) {
+ String imageUri = m.group(1);
+ if (!imageUri.equals("")) {
+ MediaFile mediaFile = WordPress.wpDB.getMediaFile(imageUri, mPost);
+ if (mediaFile != null) {
+ // Get image thumbnail for notification icon
+ Bitmap imageIcon = ImageUtils.getWPImageSpanThumbnailFromFilePath(
+ mContext,
+ imageUri,
+ DisplayUtils.dpToPx(mContext, 128)
+ );
+
+ // Crop the thumbnail to be squared in the center
+ if (imageIcon != null) {
+ int squaredSize = DisplayUtils.dpToPx(mContext, 64);
+ imageIcon = ThumbnailUtils.extractThumbnail(imageIcon, squaredSize, squaredSize);
+ mLatestIcon = imageIcon;
+ }
+
+ mediaItemCount++;
+ mPostUploadNotifier.setCurrentMediaItem(mediaItemCount);
+ mPostUploadNotifier.updateNotificationIcon(imageIcon);
+
+ String mediaUploadOutput;
+ if (mediaFile.isVideo()) {
+ mHasVideo = true;
+ mediaUploadOutput = uploadVideo(mediaFile);
+ } else {
+ mHasImage = true;
+ mediaUploadOutput = uploadImage(mediaFile);
+ }
+
+ if (mediaUploadOutput != null) {
+ postContent = postContent.replace(tag, mediaUploadOutput);
+ } else {
+ postContent = postContent.replace(tag, "");
+ mIsMediaError = true;
+ }
+ }
+ }
+ }
+ }
+
+ return postContent;
+ }
+
+ private String uploadImage(MediaFile mediaFile) {
+ AppLog.d(T.POSTS, "uploadImage: " + mediaFile.getFilePath());
+
+ if (mediaFile.getFilePath() == null) {
+ return null;
+ }
+
+ Uri imageUri = Uri.parse(mediaFile.getFilePath());
+ File imageFile = null;
+ String mimeType = "", path = "";
+
+ if (imageUri.toString().contains("content:")) {
+ String[] projection = new String[]{Images.Media._ID, Images.Media.DATA, Images.Media.MIME_TYPE};
+
+ Cursor cur = mContext.getContentResolver().query(imageUri, projection, null, null, null);
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(Images.Media.DATA);
+ int mimeTypeColumn = cur.getColumnIndex(Images.Media.MIME_TYPE);
+
+ String thumbData = cur.getString(dataColumn);
+ mimeType = cur.getString(mimeTypeColumn);
+ imageFile = new File(thumbData);
+ path = thumbData;
+ mediaFile.setFilePath(imageFile.getPath());
+ }
+ } else { // file is not in media library
+ path = imageUri.toString().replace("file://", "");
+ imageFile = new File(path);
+ mediaFile.setFilePath(path);
+ }
+
+ // check if the file exists
+ if (imageFile == null) {
+ mErrorMessage = mContext.getString(R.string.file_not_found);
+ return null;
+ }
+
+ if (TextUtils.isEmpty(mimeType)) {
+ mimeType = MediaUtils.getMediaFileMimeType(imageFile);
+ }
+ String fileName = MediaUtils.getMediaFileName(imageFile, mimeType);
+ String fileExtension = MimeTypeMap.getFileExtensionFromUrl(fileName).toLowerCase();
+
+ int orientation = ImageUtils.getImageOrientation(mContext, path);
+
+ String resizedPictureURL = null;
+
+ // We need to upload a resized version of the picture when the blog settings != original size, or when
+ // the user has selected a smaller size for the current picture in the picture settings screen
+ // We won't resize gif images to keep them awesome.
+ boolean shouldUploadResizedVersion = false;
+ // If it's not a gif and blog don't keep original size, there is a chance we need to resize
+ if (!mimeType.equals("image/gif") && MediaUtils.getImageWidthSettingFromString(mBlog.getMaxImageWidth())
+ != Integer.MAX_VALUE) {
+ // check the picture settings
+ int pictureSettingWidth = mediaFile.getWidth();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ int[] dimensions = {imageWidth, imageHeight};
+ if (dimensions[0] != 0 && dimensions[0] != pictureSettingWidth) {
+ shouldUploadResizedVersion = true;
+ }
+ }
+
+ boolean shouldAddImageWidthCSS = false;
+
+ if (shouldUploadResizedVersion) {
+ MediaFile resizedMediaFile = new MediaFile(mediaFile);
+ // Create resized image
+ byte[] bytes = ImageUtils.createThumbnailFromUri(mContext, imageUri, resizedMediaFile.getWidth(),
+ fileExtension, orientation);
+
+ if (bytes == null) {
+ // We weren't able to resize the image, so we will upload the full size image with css to resize it
+ shouldUploadResizedVersion = false;
+ shouldAddImageWidthCSS = true;
+ } else {
+ // Save temp image
+ String tempFilePath;
+ File resizedImageFile;
+ try {
+ resizedImageFile = File.createTempFile("wp-image-", fileExtension);
+ FileOutputStream out = new FileOutputStream(resizedImageFile);
+ out.write(bytes);
+ out.close();
+ tempFilePath = resizedImageFile.getPath();
+ } catch (IOException e) {
+ AppLog.w(T.POSTS, "failed to create image temp file");
+ mErrorMessage = mContext.getString(R.string.error_media_upload);
+ return null;
+ }
+
+ // upload resized picture
+ if (!TextUtils.isEmpty(tempFilePath)) {
+ resizedMediaFile.setFilePath(tempFilePath);
+ Map<String, Object> parameters = new HashMap<String, Object>();
+
+ parameters.put("name", fileName);
+ parameters.put("type", mimeType);
+ parameters.put("bits", resizedMediaFile);
+ parameters.put("overwrite", true);
+ resizedPictureURL = uploadImageFile(parameters, resizedMediaFile, mBlog);
+ if (resizedPictureURL == null) {
+ AppLog.w(T.POSTS, "failed to upload resized picture");
+ return null;
+ } else if (resizedImageFile.exists()) {
+ resizedImageFile.delete();
+ }
+ } else {
+ AppLog.w(T.POSTS, "failed to create resized picture");
+ mErrorMessage = mContext.getString(R.string.out_of_memory);
+ return null;
+ }
+ }
+ }
+
+ String fullSizeUrl = null;
+ // Upload the full size picture if "Original Size" is selected in settings,
+ // or if 'link to full size' is checked.
+ if (!shouldUploadResizedVersion || mBlog.isFullSizeImage()) {
+ Map<String, Object> parameters = new HashMap<String, Object>();
+ parameters.put("name", fileName);
+ parameters.put("type", mimeType);
+ parameters.put("bits", mediaFile);
+ parameters.put("overwrite", true);
+
+ fullSizeUrl = uploadImageFile(parameters, mediaFile, mBlog);
+ if (fullSizeUrl == null) {
+ mErrorMessage = mContext.getString(R.string.error_media_upload);
+ return null;
+ }
+ }
+
+ return mediaFile.getImageHtmlForUrls(fullSizeUrl, resizedPictureURL, shouldAddImageWidthCSS);
+ }
+
+ private String uploadVideo(MediaFile mediaFile) {
+ // create temp file for media upload
+ String tempFileName = "wp-" + System.currentTimeMillis();
+ try {
+ mContext.openFileOutput(tempFileName, Context.MODE_PRIVATE);
+ } catch (FileNotFoundException e) {
+ mErrorMessage = getResources().getString(R.string.file_error_create);
+ return null;
+ }
+
+ if (mediaFile.getFilePath() == null) {
+ mErrorMessage = mContext.getString(R.string.error_media_upload);
+ return null;
+ }
+
+ Uri videoUri = Uri.parse(mediaFile.getFilePath());
+ File videoFile = null;
+ String mimeType = "", xRes = "", yRes = "";
+
+ if (videoUri.toString().contains("content:")) {
+ String[] projection = new String[]{Video.Media._ID, Video.Media.DATA, Video.Media.MIME_TYPE,
+ Video.Media.RESOLUTION};
+ Cursor cur = mContext.getContentResolver().query(videoUri, projection, null, null, null);
+
+ if (cur != null && cur.moveToFirst()) {
+ int dataColumn = cur.getColumnIndex(Video.Media.DATA);
+ int mimeTypeColumn = cur.getColumnIndex(Video.Media.MIME_TYPE);
+ int resolutionColumn = cur.getColumnIndex(Video.Media.RESOLUTION);
+
+ mediaFile = new MediaFile();
+
+ String thumbData = cur.getString(dataColumn);
+ mimeType = cur.getString(mimeTypeColumn);
+
+ videoFile = new File(thumbData);
+ mediaFile.setFilePath(videoFile.getPath());
+ String resolution = cur.getString(resolutionColumn);
+ if (resolution != null) {
+ String[] resolutions = resolution.split("x");
+ if (resolutions.length >= 2) {
+ xRes = resolutions[0];
+ yRes = resolutions[1];
+ }
+ } else {
+ // set the width of the video to the thumbnail width, else 640x480
+ if (MediaUtils.getImageWidthSettingFromString(mBlog.getMaxImageWidth()) != Integer.MAX_VALUE) {
+ xRes = mBlog.getMaxImageWidth();
+ yRes = String.valueOf(Math.round(Integer.valueOf(mBlog.getMaxImageWidth()) * 0.75));
+ } else {
+ xRes = "640";
+ yRes = "480";
+ }
+ }
+ }
+ } else { // file is not in media library
+ String filePath = videoUri.toString().replace("file://", "");
+ mediaFile.setFilePath(filePath);
+ videoFile = new File(filePath);
+ }
+
+ if (videoFile == null) {
+ mErrorMessage = mContext.getResources().getString(R.string.error_media_upload);
+ return null;
+ }
+
+ if (TextUtils.isEmpty(mimeType)) {
+ mimeType = MediaUtils.getMediaFileMimeType(videoFile);
+ }
+ String videoName = MediaUtils.getMediaFileName(videoFile, mimeType);
+
+ // try to upload the video
+ Map<String, Object> m = new HashMap<String, Object>();
+ m.put("name", videoName);
+ m.put("type", mimeType);
+ m.put("bits", mediaFile);
+ m.put("overwrite", true);
+
+ Object[] params = {1, mBlog.getUsername(), mBlog.getPassword(), m};
+
+ File tempFile;
+ try {
+ String fileExtension = MimeTypeMap.getFileExtensionFromUrl(videoName);
+ tempFile = createTempUploadFile(fileExtension);
+ } catch (IOException e) {
+ mErrorMessage = getResources().getString(R.string.file_error_create);
+ return null;
+ }
+
+ Object result = uploadFileHelper(params, tempFile);
+ Map<?, ?> resultMap = (HashMap<?, ?>) result;
+ if (resultMap != null && resultMap.containsKey("url")) {
+ String resultURL = resultMap.get("url").toString();
+ if (resultMap.containsKey(MediaFile.VIDEOPRESS_SHORTCODE_ID)) {
+ resultURL = resultMap.get(MediaFile.VIDEOPRESS_SHORTCODE_ID).toString() + "\n";
+ } else {
+ resultURL = String.format(
+ "<video width=\"%s\" height=\"%s\" controls=\"controls\"><source src=\"%s\" type=\"%s\" /><a href=\"%s\">Click to view video</a>.</video>",
+ xRes, yRes, resultURL, mimeType, resultURL);
+ }
+
+ return resultURL;
+ } else {
+ mErrorMessage = mContext.getResources().getString(R.string.error_media_upload);
+ return null;
+ }
+ }
+
+
+ private void setUploadPostErrorMessage(Exception e) {
+ mErrorMessage = String.format(mContext.getResources().getText(R.string.error_upload).toString(),
+ mPost.isPage() ? mContext.getResources().getText(R.string.page).toString() :
+ mContext.getResources().getText(R.string.post).toString()) + " " + e.getMessage();
+ mIsMediaError = false;
+ AppLog.e(T.EDITOR, mErrorMessage, e);
+ }
+
+ private String uploadImageFile(Map<String, Object> pictureParams, MediaFile mf, Blog blog) {
+ // create temporary upload file
+ File tempFile;
+ try {
+ String fileExtension = MimeTypeMap.getFileExtensionFromUrl(mf.getFileName());
+ tempFile = createTempUploadFile(fileExtension);
+ } catch (IOException e) {
+ mIsMediaError = true;
+ mErrorMessage = mContext.getString(R.string.file_not_found);
+ return null;
+ }
+
+ Object[] params = {1, blog.getUsername(), blog.getPassword(), pictureParams};
+ Object result = uploadFileHelper(params, tempFile);
+ if (result == null) {
+ mIsMediaError = true;
+ return null;
+ }
+
+ Map<?, ?> contentHash = (HashMap<?, ?>) result;
+ String pictureURL = contentHash.get("url").toString();
+
+ if (mf.isFeatured()) {
+ try {
+ if (contentHash.get("id") != null) {
+ featuredImageID = Integer.parseInt(contentHash.get("id").toString());
+ if (!mf.isFeaturedInPost())
+ return "";
+ }
+ } catch (NumberFormatException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+
+ return pictureURL;
+ }
+
+ private Object uploadFileHelper(Object[] params, final File tempFile) {
+ // Create listener for tracking upload progress in the notification
+ if (mClient instanceof XMLRPCClient) {
+ XMLRPCClient xmlrpcClient = (XMLRPCClient) mClient;
+ xmlrpcClient.setOnBytesUploadedListener(new XMLRPCClient.OnBytesUploadedListener() {
+ @Override
+ public void onBytesUploaded(long uploadedBytes) {
+ if (tempFile.length() == 0) {
+ return;
+ }
+ float percentage = (uploadedBytes * 100) / tempFile.length();
+ mPostUploadNotifier.updateNotificationProgress(percentage);
+ }
+ });
+ }
+
+ try {
+ return mClient.call(Method.UPLOAD_FILE, params, tempFile);
+ } catch (XMLRPCException e) {
+ // well formed XML-RPC response from the server, but it's an error. Ok to print the error message
+ AppLog.e(T.API, e);
+ mErrorMessage = mContext.getResources().getString(R.string.error_media_upload) + ": " + e.getMessage();
+ return null;
+ } catch (IOException e) {
+ // I/O-related error. Show a generic connection error message
+ AppLog.e(T.API, e);
+ mErrorMessage = mContext.getResources().getString(R.string.error_media_upload_connection);
+ return null;
+ } catch (XmlPullParserException e) {
+ // XML-RPC response isn't well formed or valid. DO NOT print the real error message
+ AppLog.e(T.API, e);
+ mErrorMessage = mContext.getResources().getString(R.string.error_media_upload);
+ return null;
+ } finally {
+ // remove the temporary upload file now that we're done with it
+ if (tempFile != null && tempFile.exists()) {
+ tempFile.delete();
+ }
+ }
+ }
+ }
+
+ private File createTempUploadFile(String fileExtension) throws IOException {
+ return File.createTempFile("wp-", fileExtension, mContext.getCacheDir());
+ }
+
+ private class PostUploadNotifier {
+ private final NotificationManager mNotificationManager;
+ private final Builder mNotificationBuilder;
+ private final int mNotificationId;
+ private int mNotificationErrorId = 0;
+ private int mTotalMediaItems;
+ private int mCurrentMediaItem;
+ private float mItemProgressSize;
+
+ public PostUploadNotifier(Post post, String title, String message) {
+ // add the uploader to the notification bar
+ mNotificationManager = (NotificationManager) SystemServiceFactory.get(mContext,
+ Context.NOTIFICATION_SERVICE);
+ mNotificationBuilder = new Notification.Builder(getApplicationContext());
+ mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_upload);
+ if (title != null) {
+ mNotificationBuilder.setContentTitle(title);
+ }
+ if (message != null) {
+ mNotificationBuilder.setContentText(message);
+ }
+ mNotificationId = (new Random()).nextInt() + post.getLocalTableBlogId();
+ startForeground(mNotificationId, mNotificationBuilder.build());
+ }
+
+ public void updateNotificationIcon(Bitmap icon) {
+ if (icon != null) {
+ mNotificationBuilder.setLargeIcon(icon);
+ }
+ doNotify(mNotificationId, mNotificationBuilder.build());
+ }
+
+ public void cancelNotification() {
+ mNotificationManager.cancel(mNotificationId);
+ }
+
+ public void updateNotificationSuccess(Post post, Bitmap largeIcon, boolean isFirstPublishing) {
+ AppLog.d(T.POSTS, "updateNotificationSuccess");
+
+ // Get the sharableUrl
+ String sharableUrl = WPMeShortlinks.getPostShortlink(post);
+ if (sharableUrl == null && !TextUtils.isEmpty(post.getPermaLink())) {
+ sharableUrl = post.getPermaLink();
+ }
+
+ // Notification builder
+ Builder notificationBuilder = new Notification.Builder(getApplicationContext());
+ String notificationTitle = (String) (post.isPage() ? mContext.getResources().getText(R.string
+ .page_published) : mContext.getResources().getText(R.string.post_published));
+ if (!isFirstPublishing) {
+ notificationTitle = (String) (post.isPage() ? mContext.getResources().getText(R.string
+ .page_updated) : mContext.getResources().getText(R.string.post_updated));
+ }
+ notificationBuilder.setSmallIcon(android.R.drawable.stat_sys_upload_done);
+ if (largeIcon == null) {
+ notificationBuilder.setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(),
+ R.mipmap.app_icon));
+ } else {
+ notificationBuilder.setLargeIcon(largeIcon);
+ }
+ notificationBuilder.setContentTitle(notificationTitle);
+ notificationBuilder.setContentText(post.getTitle());
+ notificationBuilder.setAutoCancel(true);
+
+ // Tap notification intent (open the post list)
+ Intent notificationIntent = new Intent(mContext, PostsListActivity.class);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ notificationIntent.putExtra(PostsListActivity.EXTRA_BLOG_LOCAL_ID, post.getLocalTableBlogId());
+ notificationIntent.putExtra(PostsListActivity.EXTRA_VIEW_PAGES, post.isPage());
+ PendingIntent pendingIntentPost = PendingIntent.getActivity(mContext, 0,
+ notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ notificationBuilder.setContentIntent(pendingIntentPost);
+
+ // Share intent - started if the user tap the share link button - only if the link exist
+ int notificationId = getNotificationIdForPost(post);
+ if (sharableUrl != null && post.getStatusEnum() == PostStatus.PUBLISHED) {
+ Intent shareIntent = new Intent(mContext, ShareAndDismissNotificationReceiver.class);
+ shareIntent.putExtra(ShareAndDismissNotificationReceiver.NOTIFICATION_ID_KEY, notificationId);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, sharableUrl);
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, post.getTitle());
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, shareIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ notificationBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_action),
+ pendingIntent);
+ }
+ doNotify(notificationId, notificationBuilder.build());
+ }
+
+ private int getNotificationIdForPost(Post post) {
+ int remotePostId = StringUtils.stringToInt(post.getRemotePostId());
+ // We can't use the local table post id here because it can change between first post (local draft) to
+ // first edit (post pulled from the server)
+ return post.getLocalTableBlogId() + remotePostId;
+ }
+
+ public void updateNotificationError(String mErrorMessage, boolean isMediaError, boolean isPage) {
+ AppLog.d(T.POSTS, "updateNotificationError: " + mErrorMessage);
+
+ Builder notificationBuilder = new Notification.Builder(getApplicationContext());
+ String postOrPage = (String) (isPage ? mContext.getResources().getText(R.string.page_id)
+ : mContext.getResources().getText(R.string.post_id));
+ Intent notificationIntent = new Intent(mContext, PostsListActivity.class);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ notificationIntent.putExtra(PostsListActivity.EXTRA_VIEW_PAGES, isPage);
+ notificationIntent.putExtra(PostsListActivity.EXTRA_ERROR_MSG, mErrorMessage);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0,
+ notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ String errorText = mContext.getResources().getText(R.string.upload_failed).toString();
+ if (isMediaError) {
+ errorText = mContext.getResources().getText(R.string.media) + " "
+ + mContext.getResources().getText(R.string.error);
+ }
+
+ notificationBuilder.setSmallIcon(android.R.drawable.stat_notify_error);
+ notificationBuilder.setContentTitle((isMediaError) ? errorText :
+ mContext.getResources().getText(R.string.upload_failed));
+ notificationBuilder.setContentText((isMediaError) ? mErrorMessage : postOrPage + " " + errorText
+ + ": " + mErrorMessage);
+ notificationBuilder.setContentIntent(pendingIntent);
+ notificationBuilder.setAutoCancel(true);
+ if (mNotificationErrorId == 0) {
+ mNotificationErrorId = mNotificationId + (new Random()).nextInt();
+ }
+ doNotify(mNotificationErrorId, notificationBuilder.build());
+ }
+
+ public void updateNotificationProgress(float progress) {
+ if (mTotalMediaItems == 0) {
+ return;
+ }
+
+ // Simple way to show progress of entire post upload
+ // Would be better if we could get total bytes for all media items.
+ double currentChunkProgress = (mItemProgressSize * progress) / 100;
+
+ if (mCurrentMediaItem > 1) {
+ currentChunkProgress += mItemProgressSize * (mCurrentMediaItem - 1);
+ }
+
+ mNotificationBuilder.setProgress(100, (int)Math.ceil(currentChunkProgress), false);
+ doNotify(mNotificationId, mNotificationBuilder.build());
+ }
+
+ private synchronized void doNotify(int id, Notification notification) {
+ try {
+ mNotificationManager.notify(id, notification);
+ } catch (RuntimeException runtimeException) {
+ CrashlyticsUtils.logException(runtimeException, CrashlyticsUtils.ExceptionType.SPECIFIC,
+ AppLog.T.UTILS, "See issue #2858 / #3966");
+ AppLog.d(T.POSTS, "See issue #2858 / #3966; notify failed with:" + runtimeException);
+ }
+ }
+
+ public void setTotalMediaItems(int totalMediaItems) {
+ if (totalMediaItems <= 0) {
+ totalMediaItems = 1;
+ }
+
+ mTotalMediaItems = totalMediaItems;
+ mItemProgressSize = 100.0f / mTotalMediaItems;
+ }
+
+ public void setCurrentMediaItem(int currentItem) {
+ mCurrentMediaItem = currentItem;
+
+ mNotificationBuilder.setContentText(String.format(getString(R.string.uploading_total), mCurrentMediaItem,
+ mTotalMediaItems));
+ }
+ }
+}