diff options
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.java | 1026 |
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)); + } + } +} |