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

main
Ivan Kupalov 7 years ago committed by Konrad Pozniak
parent 9bc67f3485
commit 387b37e0a8
  1. 2
      app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
  2. 51
      app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
  3. 78
      app/src/main/java/com/keylesspalace/tusky/network/ProgressRequestBody.java
  4. 93
      app/src/main/java/com/keylesspalace/tusky/view/ProgressImageView.java

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

@ -86,6 +86,7 @@ import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Media; import com.keylesspalace.tusky.entity.Media;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.fragment.ComposeOptionsFragment; import com.keylesspalace.tusky.fragment.ComposeOptionsFragment;
import com.keylesspalace.tusky.network.ProgressRequestBody;
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.IOUtils;
@ -96,6 +97,7 @@ import com.keylesspalace.tusky.util.SpanUtils;
import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EditTextTyped; import com.keylesspalace.tusky.view.EditTextTyped;
import com.keylesspalace.tusky.view.ProgressImageView;
import com.keylesspalace.tusky.view.RoundedTransformation; import com.keylesspalace.tusky.view.RoundedTransformation;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target; import com.squareup.picasso.Target;
@ -115,7 +117,6 @@ import java.util.Locale;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
@ -317,7 +318,8 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
if (!TextUtils.isEmpty(savedJsonUrls)) { if (!TextUtils.isEmpty(savedJsonUrls)) {
// try to redo a list of media // try to redo a list of media
loadedDraftMediaUris = new Gson().fromJson(savedJsonUrls, loadedDraftMediaUris = new Gson().fromJson(savedJsonUrls,
new TypeToken<ArrayList<String>>() {}.getType()); new TypeToken<ArrayList<String>>() {
}.getType());
} }
int savedTootUid = intent.getIntExtra("saved_toot_uid", 0); int savedTootUid = intent.getIntExtra("saved_toot_uid", 0);
@ -650,6 +652,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
/** /**
* AB={xA|xB} * AB={xA|xB}
*
* @return all elements of set A that are not in set B. * @return all elements of set A that are not in set B.
*/ */
private static List<String> setDifference(List<String> a, List<String> 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"); String savedJsonUrls = getIntent().getStringExtra("saved_json_urls");
if (!TextUtils.isEmpty(savedJsonUrls)) { if (!TextUtils.isEmpty(savedJsonUrls)) {
existingUris = new Gson().fromJson(savedJsonUrls, existingUris = new Gson().fromJson(savedJsonUrls,
new TypeToken<ArrayList<String>>() {}.getType()); new TypeToken<ArrayList<String>>() {
}.getType());
} }
final TootEntity toot = new TootEntity(); final TootEntity toot = new TootEntity();
@ -683,7 +687,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
if (!ListUtils.isEmpty(savedList)) { if (!ListUtils.isEmpty(savedList)) {
String json = new Gson().toJson(savedList); String json = new Gson().toJson(savedList);
toot.setUrls(json); toot.setUrls(json);
if(!ListUtils.isEmpty(existingUris)) { if (!ListUtils.isEmpty(existingUris)) {
deleteMedia(setDifference(existingUris, savedList)); deleteMedia(setDifference(existingUris, savedList));
} }
} else { } else {
@ -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) { 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; item.readyStage = readyStage;
ImageView view = item.preview; ImageView view = item.preview;
Resources resources = getResources(); Resources resources = getResources();
@ -1261,7 +1265,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
private void uploadMedia(final QueuedMedia item) { private void uploadMedia(final QueuedMedia item) {
item.readyStage = QueuedMedia.ReadyStage.UPLOADING; item.readyStage = QueuedMedia.ReadyStage.UPLOADING;
final String mimeType = getContentResolver().getType(item.uri); String mimeType = getContentResolver().getType(item.uri);
MimeTypeMap map = MimeTypeMap.getSingleton(); MimeTypeMap map = MimeTypeMap.getSingleton();
String fileExtension = map.getExtensionFromMimeType(mimeType); String fileExtension = map.getExtensionFromMimeType(mimeType);
final String filename = String.format("%s_%s_%s.%s", 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); if (mimeType == null) mimeType = "multipart/form-data";
MultipartBody.Part body = MultipartBody.Part.createFormData("file", filename, requestFile);
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 = mastodonApi.uploadMedia(body);
item.uploadRequest.enqueue(new Callback<Media>() { item.uploadRequest.enqueue(new Callback<Media>() {
@Override @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()) { if (response.isSuccessful()) {
onUploadSuccess(item, response.body()); onUploadSuccess(item, response.body());
} else { } else {
@ -1307,7 +1333,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
} }
@Override @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()); Log.d(TAG, "Upload request failed. " + t.getMessage());
onUploadFailure(item, call.isCanceled()); onUploadFailure(item, call.isCanceled());
} }
@ -1316,6 +1342,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
private void onUploadSuccess(final QueuedMedia item, Media media) { private void onUploadSuccess(final QueuedMedia item, Media media) {
item.id = media.id; item.id = media.id;
item.preview.setProgress(-1);
item.readyStage = QueuedMedia.ReadyStage.UPLOADED; item.readyStage = QueuedMedia.ReadyStage.UPLOADED;
/* Add the upload URL to the text field. Also, keep a reference to the span so if the user /* 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 { private static class QueuedMedia {
Type type; Type type;
ImageView preview; ProgressImageView preview;
Uri uri; Uri uri;
String id; String id;
Call<Media> uploadRequest; Call<Media> uploadRequest;
@ -1526,7 +1553,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
byte[] content; byte[] content;
long mediaSize; long mediaSize;
QueuedMedia(Type type, Uri uri, ImageView preview, long mediaSize) { QueuedMedia(Type type, Uri uri, ProgressImageView preview, long mediaSize) {
this.type = type; this.type = type;
this.uri = uri; this.uri = uri;
this.preview = preview; this.preview = preview;

@ -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;
}
}

@ -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();
}
}
Loading…
Cancel
Save