Add media upload progress. Closes #412 (#426)

This commit is contained in:
Ivan Kupalov 2017-10-30 01:18:45 +04:00 committed by Konrad Pozniak
parent d2a5dcc144
commit 15e37576e5
4 changed files with 215 additions and 17 deletions

View File

@ -145,7 +145,7 @@ public class BaseActivity extends AppCompatActivity {
if (BuildConfig.DEBUG) {
okBuilder.addInterceptor(
new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY));
new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC));
}
Retrofit retrofit = new Retrofit.Builder().baseUrl(getBaseUrl())

View File

@ -86,6 +86,7 @@ import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Media;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.fragment.ComposeOptionsFragment;
import com.keylesspalace.tusky.network.ProgressRequestBody;
import com.keylesspalace.tusky.util.CountUpDownLatch;
import com.keylesspalace.tusky.util.DownsizeImageTask;
import com.keylesspalace.tusky.util.IOUtils;
@ -96,6 +97,7 @@ import com.keylesspalace.tusky.util.SpanUtils;
import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EditTextTyped;
import com.keylesspalace.tusky.view.ProgressImageView;
import com.keylesspalace.tusky.view.RoundedTransformation;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
@ -115,7 +117,6 @@ import java.util.Locale;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -317,7 +318,8 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
if (!TextUtils.isEmpty(savedJsonUrls)) {
// try to redo a list of media
loadedDraftMediaUris = new Gson().fromJson(savedJsonUrls,
new TypeToken<ArrayList<String>>() {}.getType());
new TypeToken<ArrayList<String>>() {
}.getType());
}
int savedTootUid = intent.getIntExtra("saved_toot_uid", 0);
@ -399,7 +401,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
statusAlreadyInFlight = false;
// These can only be added after everything affected by the media queue is initialized.
if (!ListUtils.isEmpty(loadedDraftMediaUris)) {
if (!ListUtils.isEmpty(loadedDraftMediaUris)) {
for (String uriString : loadedDraftMediaUris) {
Uri uri = Uri.parse(uriString);
long mediaSize = MediaUtils.getMediaSize(getContentResolver(), uri);
@ -650,6 +652,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
/**
* AB={xA|xB}
*
* @return all elements of set A that are not in set B.
*/
private static List<String> setDifference(List<String> a, List<String> b) {
@ -672,7 +675,8 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
String savedJsonUrls = getIntent().getStringExtra("saved_json_urls");
if (!TextUtils.isEmpty(savedJsonUrls)) {
existingUris = new Gson().fromJson(savedJsonUrls,
new TypeToken<ArrayList<String>>() {}.getType());
new TypeToken<ArrayList<String>>() {
}.getType());
}
final TootEntity toot = new TootEntity();
@ -683,7 +687,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
if (!ListUtils.isEmpty(savedList)) {
String json = new Gson().toJson(savedList);
toot.setUrls(json);
if(!ListUtils.isEmpty(existingUris)) {
if (!ListUtils.isEmpty(existingUris)) {
deleteMedia(setDifference(existingUris, savedList));
}
} else {
@ -836,7 +840,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
}
private boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags,
String[] mimeTypes) {
String[] mimeTypes) {
try {
if (currentInputContentInfo != null) {
currentInputContentInfo.releasePermission();
@ -898,7 +902,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
}
private void sendStatus(String content, String visibility, boolean sensitive,
String spoilerText) {
String spoilerText) {
ArrayList<String> mediaIds = new ArrayList<>();
for (QueuedMedia item : mediaQueued) {
@ -1068,7 +1072,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
@NonNull int[] grantResults) {
@NonNull int[] grantResults) {
switch (requestCode) {
case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: {
if (grantResults.length > 0
@ -1150,7 +1154,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
}
private void addMediaToQueue(QueuedMedia.Type type, Bitmap preview, Uri uri, long mediaSize, QueuedMedia.ReadyStage readyStage) {
final QueuedMedia item = new QueuedMedia(type, uri, new ImageView(this), mediaSize);
final QueuedMedia item = new QueuedMedia(type, uri, new ProgressImageView(this), mediaSize);
item.readyStage = readyStage;
ImageView view = item.preview;
Resources resources = getResources();
@ -1261,7 +1265,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
private void uploadMedia(final QueuedMedia item) {
item.readyStage = QueuedMedia.ReadyStage.UPLOADING;
final String mimeType = getContentResolver().getType(item.uri);
String mimeType = getContentResolver().getType(item.uri);
MimeTypeMap map = MimeTypeMap.getSingleton();
String fileExtension = map.getExtensionFromMimeType(mimeType);
final String filename = String.format("%s_%s_%s.%s",
@ -1290,14 +1294,36 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
}
}
RequestBody requestFile = RequestBody.create(MediaType.parse(mimeType), content);
MultipartBody.Part body = MultipartBody.Part.createFormData("file", filename, requestFile);
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
new ProgressRequestBody.UploadCallback() { // may reference activity longer than I would like to
int lastProgress = -1;
@Override
public void onProgressUpdate(final int percentage) {
if (percentage != lastProgress) {
runOnUiThread(new Runnable() {
@Override
public void run() {
item.preview.setProgress(percentage);
}
});
}
lastProgress = percentage;
}
});
MultipartBody.Part body = MultipartBody.Part.createFormData("file", filename, fileBody);
item.uploadRequest = mastodonApi.uploadMedia(body);
item.uploadRequest.enqueue(new Callback<Media>() {
@Override
public void onResponse(Call<Media> call, retrofit2.Response<Media> response) {
public void onResponse(@NonNull Call<Media> call, @NonNull retrofit2.Response<Media> response) {
if (response.isSuccessful()) {
onUploadSuccess(item, response.body());
} else {
@ -1307,7 +1333,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
}
@Override
public void onFailure(Call<Media> call, Throwable t) {
public void onFailure(@NonNull Call<Media> call, @NonNull Throwable t) {
Log.d(TAG, "Upload request failed. " + t.getMessage());
onUploadFailure(item, call.isCanceled());
}
@ -1316,6 +1342,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
private void onUploadSuccess(final QueuedMedia item, Media media) {
item.id = media.id;
item.preview.setProgress(-1);
item.readyStage = QueuedMedia.ReadyStage.UPLOADED;
/* Add the upload URL to the text field. Also, keep a reference to the span so if the user
@ -1517,7 +1544,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
private static class QueuedMedia {
Type type;
ImageView preview;
ProgressImageView preview;
Uri uri;
String id;
Call<Media> uploadRequest;
@ -1526,7 +1553,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
byte[] content;
long mediaSize;
QueuedMedia(Type type, Uri uri, ImageView preview, long mediaSize) {
QueuedMedia(Type type, Uri uri, ProgressImageView preview, long mediaSize) {
this.type = type;
this.uri = uri;
this.preview = preview;

View File

@ -0,0 +1,78 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.network;
import android.support.annotation.NonNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
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 MediaType mediaType;
private boolean shouldIgnoreThisPass;
private static final int DEFAULT_BUFFER_SIZE = 2048;
public interface UploadCallback {
void onProgressUpdate(int percentage);
}
public ProgressRequestBody(final byte[] content, final MediaType mediaType, boolean shouldIgnoreFirst, final UploadCallback listener) {
this.content = content;
this.mediaType = mediaType;
mListener = listener;
shouldIgnoreThisPass = shouldIgnoreFirst;
}
@Override
public MediaType contentType() {
return mediaType;
}
@Override
public long contentLength() throws IOException {
return content.length;
}
@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));
}
uploaded += read;
sink.write(buffer, 0, read);
}
} finally {
in.close();
}
shouldIgnoreThisPass = false;
}
}

View File

@ -0,0 +1,93 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;
import com.keylesspalace.tusky.R;
import com.varunest.sparkbutton.helpers.Utils;
public final class ProgressImageView extends AppCompatImageView {
private int progress = -1;
private RectF progressRect = new RectF();
private RectF biggerRect = new RectF();
private Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint clearPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public ProgressImageView(Context context) {
super(context);
init();
}
public ProgressImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ProgressImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
circlePaint.setColor(ContextCompat.getColor(getContext(), R.color.colorPrimary));
circlePaint.setStrokeWidth(Utils.dpToPx(getContext(), 4));
circlePaint.setStyle(Paint.Style.STROKE);
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
}
public void setProgress(int progress) {
this.progress = progress;
if (progress != -1) {
setColorFilter(Color.rgb(123, 123, 123), PorterDuff.Mode.MULTIPLY);
} else {
clearColorFilter();
}
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (progress == -1) {
return;
}
float angle = (progress / 100f) * 360 - 90;
float halfWidth = canvas.getWidth() / 2;
float halfHeight = canvas.getHeight() / 2;
progressRect.set(halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f);
biggerRect.set(progressRect);
int margin = 8;
biggerRect.set(progressRect.left - margin, progressRect.top - margin, progressRect.right + margin, progressRect.bottom + margin);
canvas.saveLayer(biggerRect, null, Canvas.ALL_SAVE_FLAG);
canvas.drawOval(progressRect, circlePaint);
canvas.drawArc(biggerRect, angle, 360 - angle - 90, true, clearPaint);
canvas.restore();
}
}