dont hold whole file content in memory when uploading media

This commit is contained in:
Conny Duck 2018-09-07 19:57:25 +02:00 committed by Konrad Pozniak
parent 90ef078dd0
commit 669153089a
3 changed files with 45 additions and 58 deletions

View file

@ -101,7 +101,6 @@ import com.keylesspalace.tusky.network.ProgressRequestBody;
import com.keylesspalace.tusky.service.SendTootService; import com.keylesspalace.tusky.service.SendTootService;
import com.keylesspalace.tusky.util.CountUpDownLatch; import com.keylesspalace.tusky.util.CountUpDownLatch;
import com.keylesspalace.tusky.util.DownsizeImageTask; import com.keylesspalace.tusky.util.DownsizeImageTask;
import com.keylesspalace.tusky.util.IOUtils;
import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.MediaUtils; import com.keylesspalace.tusky.util.MediaUtils;
import com.keylesspalace.tusky.util.MentionTokenizer; import com.keylesspalace.tusky.util.MentionTokenizer;
@ -124,7 +123,6 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -996,8 +994,8 @@ public final class ComposeActivity
@NonNull @NonNull
private File createNewImageFile() throws IOException { private File createNewImageFile() throws IOException {
// Create an image file name // Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); String randomId = StringUtils.randomAlphanumericString(12);
String imageFileName = "Tusky_" + timeStamp + "_"; String imageFileName = "Tusky_" + randomId + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
return File.createTempFile( return File.createTempFile(
imageFileName, /* prefix */ imageFileName, /* prefix */
@ -1094,7 +1092,7 @@ public final class ComposeActivity
} else { } else {
uploadMedia(item); uploadMedia(item);
} }
} catch (FileNotFoundException e) { } catch (IOException e) {
onUploadFailure(item, false); 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; item.readyStage = QueuedMedia.ReadyStage.DOWNSIZING;
new DownsizeImageTask(STATUS_IMAGE_SIZE_LIMIT, getContentResolver(), new DownsizeImageTask(STATUS_IMAGE_SIZE_LIMIT, getContentResolver(), createNewImageFile(),
new DownsizeImageTask.Listener() { new DownsizeImageTask.Listener() {
@Override @Override
public void onSuccess(List<byte[]> contentList) { public void onSuccess(File tempFile) {
item.content = contentList.get(0); item.uri = FileProvider.getUriForFile(
ComposeActivity.this,
BuildConfig.APPLICATION_ID+".fileprovider",
tempFile);
uploadMedia(item); uploadMedia(item);
} }
@ -1259,32 +1260,20 @@ public final class ComposeActivity
StringUtils.randomAlphanumericString(10), StringUtils.randomAlphanumericString(10),
fileExtension); fileExtension);
byte[] content = item.content; InputStream stream;
if (content == null) { try {
InputStream stream; stream = getContentResolver().openInputStream(item.uri);
} catch (FileNotFoundException e) {
try { Log.w(TAG, e);
stream = getContentResolver().openInputStream(item.uri); return;
} catch (FileNotFoundException e) {
Log.d(TAG, Log.getStackTraceString(e));
return;
}
content = MediaUtils.inputStreamGetBytes(stream);
IOUtils.closeQuietly(stream);
if (content == null) {
return;
}
} }
if (mimeType == null) mimeType = "multipart/form-data"; if (mimeType == null) mimeType = "multipart/form-data";
item.preview.setProgress(0); item.preview.setProgress(0);
ProgressRequestBody fileBody = new ProgressRequestBody(content, MediaType.parse(mimeType), ProgressRequestBody fileBody = new ProgressRequestBody(stream, MediaUtils.getMediaSize(getContentResolver(), item.uri), MediaType.parse(mimeType),
false, // If request body logging is enabled, pass true
new ProgressRequestBody.UploadCallback() { // may reference activity longer than I would like to new ProgressRequestBody.UploadCallback() { // may reference activity longer than I would like to
int lastProgress = -1; int lastProgress = -1;
@ -1544,7 +1533,6 @@ public final class ComposeActivity
String id; String id;
Call<Attachment> uploadRequest; Call<Attachment> uploadRequest;
ReadyStage readyStage; ReadyStage readyStage;
byte[] content;
long mediaSize; long mediaSize;
String description; String description;

View file

@ -17,18 +17,18 @@ package com.keylesspalace.tusky.network;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okio.BufferedSink; import okio.BufferedSink;
public final class ProgressRequestBody extends RequestBody { public final class ProgressRequestBody extends RequestBody {
private final byte[] content; private final InputStream content;
private final UploadCallback mListener; private final long contentLength;
private final UploadCallback uploadListener;
private final MediaType mediaType; private final MediaType mediaType;
private boolean shouldIgnoreThisPass;
private static final int DEFAULT_BUFFER_SIZE = 2048; private static final int DEFAULT_BUFFER_SIZE = 2048;
@ -36,11 +36,11 @@ public final class ProgressRequestBody extends RequestBody {
void onProgressUpdate(int percentage); 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.content = content;
this.contentLength = contentLength;
this.mediaType = mediaType; this.mediaType = mediaType;
mListener = listener; this.uploadListener = listener;
shouldIgnoreThisPass = shouldIgnoreFirst;
} }
@Override @Override
@ -50,29 +50,25 @@ public final class ProgressRequestBody extends RequestBody {
@Override @Override
public long contentLength() { public long contentLength() {
return content.length; return contentLength;
} }
@Override @Override
public void writeTo(@NonNull BufferedSink sink) throws IOException { public void writeTo(@NonNull BufferedSink sink) throws IOException {
long length = content.length;
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
ByteArrayInputStream in = new ByteArrayInputStream(content);
long uploaded = 0; long uploaded = 0;
try { try {
int read; int read;
while ((read = in.read(buffer)) != -1) { while ((read = content.read(buffer)) != -1) {
if (!shouldIgnoreThisPass) { uploadListener.onProgressUpdate((int)(100 * uploaded / contentLength));
mListener.onProgressUpdate((int)(100 * uploaded / length));
}
uploaded += read; uploaded += read;
sink.write(buffer, 0, read); sink.write(buffer, 0, read);
} }
} finally { } finally {
in.close(); content.close();
} }
shouldIgnoreThisPass = false;
} }
} }

View file

@ -21,11 +21,11 @@ import android.graphics.BitmapFactory;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import java.io.ByteArrayOutputStream; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.io.OutputStream;
import java.util.List;
/** /**
* Reduces the file size of images to fit under a given limit by resizing them, maintaining both * 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 int sizeLimit;
private ContentResolver contentResolver; private ContentResolver contentResolver;
private Listener listener; private Listener listener;
private List<byte[]> resultList; private File tempFile;
/** /**
* @param sizeLimit the maximum number of bytes each image can take * @param sizeLimit the maximum number of bytes each image can take
* @param contentResolver to resolve the specified images' URIs * @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 * @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.sizeLimit = sizeLimit;
this.contentResolver = contentResolver; this.contentResolver = contentResolver;
this.tempFile = tempFile;
this.listener = listener; this.listener = listener;
} }
@Override @Override
protected Boolean doInBackground(Uri... uris) { protected Boolean doInBackground(Uri... uris) {
resultList = new ArrayList<>();
for (Uri uri : uris) { for (Uri uri : uris) {
InputStream inputStream; InputStream inputStream;
try { try {
@ -65,8 +66,6 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(inputStream);
// Get EXIF data, for orientation info. // Get EXIF data, for orientation info.
int orientation = MediaUtils.getImageOrientation(uri, contentResolver); 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 /* 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 * 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 * 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. */ * sure it gets downsized to below the limit. */
int scaledImageSize = 1024; int scaledImageSize = 1024;
do { do {
stream.reset(); OutputStream stream;
try {
stream = new FileOutputStream(tempFile);
} catch (FileNotFoundException e) {
return false;
}
try { try {
inputStream = contentResolver.openInputStream(uri); inputStream = contentResolver.openInputStream(uri);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
@ -109,9 +113,8 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
reorientedBitmap.compress(format, 85, stream); reorientedBitmap.compress(format, 85, stream);
reorientedBitmap.recycle(); reorientedBitmap.recycle();
scaledImageSize /= 2; scaledImageSize /= 2;
} while (stream.size() > sizeLimit); } while (tempFile.length() > sizeLimit);
resultList.add(stream.toByteArray());
if (isCancelled()) { if (isCancelled()) {
return false; return false;
} }
@ -122,7 +125,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
@Override @Override
protected void onPostExecute(Boolean successful) { protected void onPostExecute(Boolean successful) {
if (successful) { if (successful) {
listener.onSuccess(resultList); listener.onSuccess(tempFile);
} else { } else {
listener.onFailure(); listener.onFailure();
} }
@ -131,7 +134,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
/** Used to communicate the results of the task. */ /** Used to communicate the results of the task. */
public interface Listener { public interface Listener {
void onSuccess(List<byte[]> contentList); void onSuccess(File file);
void onFailure(); void onFailure();
} }
} }