blob: 9b62c84b2bc73431812a72b99ceda79c32476039 [file] [log] [blame]
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));
}
}
}