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 mPostsList = new ArrayList(); 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 { 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 contentStruct = new HashMap(); // 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() + "" + moreContent; mPost.setMoreText(""); } // get rid of the p and br tags that the editor adds. if (mPost.isLocalDraft()) { descriptionContent = descriptionContent.replace("

", "").replace("

", "\n").replace("
", ""); } // 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 hLatitude = new HashMap(); Map hLongitude = new HashMap(); Map hPublic = new HashMap(); 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 properties = new HashMap(); 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 = "]+android-uri\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>"; Pattern pattern = Pattern.compile(imageTagsPattern); Matcher matcher = pattern.matcher(postContent); int totalMediaItems = 0; List imageTags = new ArrayList(); 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 parameters = new HashMap(); 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 parameters = new HashMap(); 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 m = new HashMap(); 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( "", 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 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)); } } }