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.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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue