dont hold whole file content in memory when uploading media
This commit is contained in:
		
					parent
					
						
							
								90ef078dd0
							
						
					
				
			
			
				commit
				
					
						669153089a
					
				
			
		
					 3 changed files with 45 additions and 58 deletions
				
			
		|  | @ -101,7 +101,6 @@ import com.keylesspalace.tusky.network.ProgressRequestBody; | |||
| import com.keylesspalace.tusky.service.SendTootService; | ||||
| import com.keylesspalace.tusky.util.CountUpDownLatch; | ||||
| import com.keylesspalace.tusky.util.DownsizeImageTask; | ||||
| import com.keylesspalace.tusky.util.IOUtils; | ||||
| import com.keylesspalace.tusky.util.ListUtils; | ||||
| import com.keylesspalace.tusky.util.MediaUtils; | ||||
| import com.keylesspalace.tusky.util.MentionTokenizer; | ||||
|  | @ -124,7 +123,6 @@ import java.io.File; | |||
| import java.io.FileNotFoundException; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.text.SimpleDateFormat; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
|  | @ -996,8 +994,8 @@ public final class ComposeActivity | |||
|     @NonNull | ||||
|     private File createNewImageFile() throws IOException { | ||||
|         // Create an image file name | ||||
|         String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); | ||||
|         String imageFileName = "Tusky_" + timeStamp + "_"; | ||||
|         String randomId = StringUtils.randomAlphanumericString(12); | ||||
|         String imageFileName = "Tusky_" + randomId + "_"; | ||||
|         File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); | ||||
|         return File.createTempFile( | ||||
|                 imageFileName,  /* prefix */ | ||||
|  | @ -1094,7 +1092,7 @@ public final class ComposeActivity | |||
|                 } else { | ||||
|                     uploadMedia(item); | ||||
|                 } | ||||
|             } catch (FileNotFoundException e) { | ||||
|             } catch (IOException e) { | ||||
|                 onUploadFailure(item, false); | ||||
|             } | ||||
|         } | ||||
|  | @ -1224,14 +1222,17 @@ public final class ComposeActivity | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void downsizeMedia(final QueuedMedia item) { | ||||
|     private void downsizeMedia(final QueuedMedia item) throws IOException { | ||||
|         item.readyStage = QueuedMedia.ReadyStage.DOWNSIZING; | ||||
| 
 | ||||
|         new DownsizeImageTask(STATUS_IMAGE_SIZE_LIMIT, getContentResolver(), | ||||
|         new DownsizeImageTask(STATUS_IMAGE_SIZE_LIMIT, getContentResolver(), createNewImageFile(), | ||||
|                 new DownsizeImageTask.Listener() { | ||||
|                     @Override | ||||
|                     public void onSuccess(List<byte[]> contentList) { | ||||
|                         item.content = contentList.get(0); | ||||
|                     public void onSuccess(File tempFile) { | ||||
|                         item.uri = FileProvider.getUriForFile( | ||||
|                                 ComposeActivity.this, | ||||
|                                 BuildConfig.APPLICATION_ID+".fileprovider", | ||||
|                                 tempFile); | ||||
|                         uploadMedia(item); | ||||
|                     } | ||||
| 
 | ||||
|  | @ -1259,32 +1260,20 @@ public final class ComposeActivity | |||
|                 StringUtils.randomAlphanumericString(10), | ||||
|                 fileExtension); | ||||
| 
 | ||||
|         byte[] content = item.content; | ||||
|         InputStream stream; | ||||
| 
 | ||||
|         if (content == null) { | ||||
|             InputStream stream; | ||||
| 
 | ||||
|             try { | ||||
|                 stream = getContentResolver().openInputStream(item.uri); | ||||
|             } catch (FileNotFoundException e) { | ||||
|                 Log.d(TAG, Log.getStackTraceString(e)); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             content = MediaUtils.inputStreamGetBytes(stream); | ||||
|             IOUtils.closeQuietly(stream); | ||||
| 
 | ||||
|             if (content == null) { | ||||
|                 return; | ||||
|             } | ||||
|         try { | ||||
|             stream = getContentResolver().openInputStream(item.uri); | ||||
|         } catch (FileNotFoundException e) { | ||||
|             Log.w(TAG, e); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (mimeType == null) mimeType = "multipart/form-data"; | ||||
| 
 | ||||
|         item.preview.setProgress(0); | ||||
| 
 | ||||
|         ProgressRequestBody fileBody = new ProgressRequestBody(content, MediaType.parse(mimeType), | ||||
|                 false, // If request body logging is enabled, pass true | ||||
|         ProgressRequestBody fileBody = new ProgressRequestBody(stream, MediaUtils.getMediaSize(getContentResolver(), item.uri), MediaType.parse(mimeType), | ||||
|                 new ProgressRequestBody.UploadCallback() { // may reference activity longer than I would like to | ||||
|                     int lastProgress = -1; | ||||
| 
 | ||||
|  | @ -1544,7 +1533,6 @@ public final class ComposeActivity | |||
|         String id; | ||||
|         Call<Attachment> uploadRequest; | ||||
|         ReadyStage readyStage; | ||||
|         byte[] content; | ||||
|         long mediaSize; | ||||
|         String description; | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,18 +17,18 @@ package com.keylesspalace.tusky.network; | |||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| 
 | ||||
| import okhttp3.MediaType; | ||||
| import okhttp3.RequestBody; | ||||
| import okio.BufferedSink; | ||||
| 
 | ||||
| public final class ProgressRequestBody extends RequestBody { | ||||
|     private final byte[] content; | ||||
|     private final UploadCallback mListener; | ||||
|     private final InputStream content; | ||||
|     private final long contentLength; | ||||
|     private final UploadCallback uploadListener; | ||||
|     private final MediaType mediaType; | ||||
|     private boolean shouldIgnoreThisPass; | ||||
| 
 | ||||
|     private static final int DEFAULT_BUFFER_SIZE = 2048; | ||||
| 
 | ||||
|  | @ -36,11 +36,11 @@ public final class ProgressRequestBody extends RequestBody { | |||
|         void onProgressUpdate(int percentage); | ||||
|     } | ||||
| 
 | ||||
|     public ProgressRequestBody(final byte[] content, final MediaType mediaType, boolean shouldIgnoreFirst, final UploadCallback listener) { | ||||
|     public ProgressRequestBody(final InputStream content, long contentLength, final MediaType mediaType, final UploadCallback listener) { | ||||
|         this.content = content; | ||||
|         this.contentLength = contentLength; | ||||
|         this.mediaType = mediaType; | ||||
|         mListener = listener; | ||||
|         shouldIgnoreThisPass = shouldIgnoreFirst; | ||||
|         this.uploadListener = listener; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -50,29 +50,25 @@ public final class ProgressRequestBody extends RequestBody { | |||
| 
 | ||||
|     @Override | ||||
|     public long contentLength() { | ||||
|         return content.length; | ||||
|         return contentLength; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void writeTo(@NonNull BufferedSink sink) throws IOException { | ||||
|         long length = content.length; | ||||
| 
 | ||||
|         byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; | ||||
|         ByteArrayInputStream in = new ByteArrayInputStream(content); | ||||
|         long uploaded = 0; | ||||
| 
 | ||||
|         try { | ||||
|             int read; | ||||
|             while ((read = in.read(buffer)) != -1) { | ||||
|                 if (!shouldIgnoreThisPass) { | ||||
|                     mListener.onProgressUpdate((int)(100 * uploaded / length)); | ||||
|                 } | ||||
|             while ((read = content.read(buffer)) != -1) { | ||||
|                 uploadListener.onProgressUpdate((int)(100 * uploaded / contentLength)); | ||||
| 
 | ||||
|                 uploaded += read; | ||||
|                 sink.write(buffer, 0, read); | ||||
|             } | ||||
|         } finally { | ||||
|             in.close(); | ||||
|             content.close(); | ||||
|         } | ||||
|         shouldIgnoreThisPass = false; | ||||
|     } | ||||
| } | ||||
|  | @ -21,11 +21,11 @@ import android.graphics.BitmapFactory; | |||
| import android.net.Uri; | ||||
| import android.os.AsyncTask; | ||||
| 
 | ||||
| import java.io.ByteArrayOutputStream; | ||||
| import java.io.File; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.InputStream; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.io.OutputStream; | ||||
| 
 | ||||
| /** | ||||
|  * Reduces the file size of images to fit under a given limit by resizing them, maintaining both | ||||
|  | @ -35,22 +35,23 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { | |||
|     private int sizeLimit; | ||||
|     private ContentResolver contentResolver; | ||||
|     private Listener listener; | ||||
|     private List<byte[]> resultList; | ||||
|     private File tempFile; | ||||
| 
 | ||||
|     /** | ||||
|      * @param sizeLimit the maximum number of bytes each image can take | ||||
|      * @param contentResolver to resolve the specified images' URIs | ||||
|      * @param tempFile the file where the result will be stored | ||||
|      * @param listener to whom the results are given | ||||
|      */ | ||||
|     public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, Listener listener) { | ||||
|     public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) { | ||||
|         this.sizeLimit = sizeLimit; | ||||
|         this.contentResolver = contentResolver; | ||||
|         this.tempFile = tempFile; | ||||
|         this.listener = listener; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected Boolean doInBackground(Uri... uris) { | ||||
|         resultList = new ArrayList<>(); | ||||
|         for (Uri uri : uris) { | ||||
|             InputStream inputStream; | ||||
|             try { | ||||
|  | @ -65,8 +66,6 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { | |||
|             IOUtils.closeQuietly(inputStream); | ||||
|             // Get EXIF data, for orientation info. | ||||
|             int orientation = MediaUtils.getImageOrientation(uri, contentResolver); | ||||
|             // Then use that information to determine how much to compress. | ||||
|             ByteArrayOutputStream stream = new ByteArrayOutputStream(); | ||||
|             /* Unfortunately, there isn't a determined worst case compression ratio for image | ||||
|              * formats. So, the only way to tell if they're too big is to compress them and | ||||
|              * test, and keep trying at smaller sizes. The initial estimate should be good for | ||||
|  | @ -74,7 +73,12 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { | |||
|              * sure it gets downsized to below the limit. */ | ||||
|             int scaledImageSize = 1024; | ||||
|             do { | ||||
|                 stream.reset(); | ||||
|                 OutputStream stream; | ||||
|                 try { | ||||
|                     stream = new FileOutputStream(tempFile); | ||||
|                 } catch (FileNotFoundException e) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 try { | ||||
|                     inputStream = contentResolver.openInputStream(uri); | ||||
|                 } catch (FileNotFoundException e) { | ||||
|  | @ -109,9 +113,8 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { | |||
|                 reorientedBitmap.compress(format, 85, stream); | ||||
|                 reorientedBitmap.recycle(); | ||||
|                 scaledImageSize /= 2; | ||||
|             } while (stream.size() > sizeLimit); | ||||
|             } while (tempFile.length() > sizeLimit); | ||||
| 
 | ||||
|             resultList.add(stream.toByteArray()); | ||||
|             if (isCancelled()) { | ||||
|                 return false; | ||||
|             } | ||||
|  | @ -122,7 +125,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { | |||
|     @Override | ||||
|     protected void onPostExecute(Boolean successful) { | ||||
|         if (successful) { | ||||
|             listener.onSuccess(resultList); | ||||
|             listener.onSuccess(tempFile); | ||||
|         } else { | ||||
|             listener.onFailure(); | ||||
|         } | ||||
|  | @ -131,7 +134,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { | |||
| 
 | ||||
|     /** Used to communicate the results of the task. */ | ||||
|     public interface Listener { | ||||
|         void onSuccess(List<byte[]> contentList); | ||||
|         void onSuccess(File file); | ||||
|         void onFailure(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue