Compose media attachment redesign
This commit is contained in:
parent
3aa252f681
commit
01970ab69b
|
@ -9,7 +9,7 @@ android {
|
||||||
applicationId "org.joinmastodon.android"
|
applicationId "org.joinmastodon.android"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 40
|
versionCode 41
|
||||||
versionName "1.1.3"
|
versionName "1.1.3"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ public class CacheController{
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}catch(SQLiteException x){
|
}catch(SQLiteException x){
|
||||||
Log.w(TAG, x);
|
Log.w(TAG, x);
|
||||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
|
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
|
||||||
}finally{
|
}finally{
|
||||||
closeDelayed();
|
closeDelayed();
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ public class CacheController{
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}catch(SQLiteException x){
|
}catch(SQLiteException x){
|
||||||
Log.w(TAG, x);
|
Log.w(TAG, x);
|
||||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
|
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
|
||||||
}finally{
|
}finally{
|
||||||
closeDelayed();
|
closeDelayed();
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,7 +100,7 @@ public class MastodonAPIController{
|
||||||
synchronized(req){
|
synchronized(req){
|
||||||
req.okhttpCall=null;
|
req.okhttpCall=null;
|
||||||
}
|
}
|
||||||
req.onError(e.getLocalizedMessage(), 0);
|
req.onError(e.getLocalizedMessage(), 0, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -133,7 +133,7 @@ public class MastodonAPIController{
|
||||||
}catch(JsonIOException|JsonSyntaxException x){
|
}catch(JsonIOException|JsonSyntaxException x){
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||||
req.onError(x.getLocalizedMessage(), response.code());
|
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ public class MastodonAPIController{
|
||||||
}catch(IOException x){
|
}catch(IOException x){
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
||||||
req.onError(x.getLocalizedMessage(), response.code());
|
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ public class MastodonAPIController{
|
||||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
||||||
if(error.has("details")){
|
if(error.has("details")){
|
||||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code());
|
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
||||||
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
||||||
JsonObject errorDetails=error.getAsJsonObject("details");
|
JsonObject errorDetails=error.getAsJsonObject("details");
|
||||||
for(String key:errorDetails.keySet()){
|
for(String key:errorDetails.keySet()){
|
||||||
|
@ -172,12 +172,12 @@ public class MastodonAPIController{
|
||||||
err.detailedErrors=details;
|
err.detailedErrors=details;
|
||||||
req.onError(err);
|
req.onError(err);
|
||||||
}else{
|
}else{
|
||||||
req.onError(error.get("error").getAsString(), response.code());
|
req.onError(error.get("error").getAsString(), response.code(), null);
|
||||||
}
|
}
|
||||||
}catch(JsonIOException|JsonSyntaxException x){
|
}catch(JsonIOException|JsonSyntaxException x){
|
||||||
req.onError(response.code()+" "+response.message(), response.code());
|
req.onError(response.code()+" "+response.message(), response.code(), x);
|
||||||
}catch(Exception x){
|
}catch(Exception x){
|
||||||
req.onError("Error parsing an API error", response.code());
|
req.onError("Error parsing an API error", response.code(), x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}catch(Exception x){
|
}catch(Exception x){
|
||||||
|
@ -189,7 +189,7 @@ public class MastodonAPIController{
|
||||||
}catch(Exception x){
|
}catch(Exception x){
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
||||||
req.onError(x.getLocalizedMessage(), 0);
|
req.onError(x.getLocalizedMessage(), 0, x);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||||
account.getApiController().submitRequest(this);
|
account.getApiController().submitRequest(this);
|
||||||
}catch(Exception x){
|
}catch(Exception x){
|
||||||
Log.e(TAG, "exec: this shouldn't happen, but it still did", x);
|
Log.e(TAG, "exec: this shouldn't happen, but it still did", x);
|
||||||
invokeErrorCallback(new MastodonErrorResponse(x.getLocalizedMessage(), -1));
|
invokeErrorCallback(new MastodonErrorResponse(x.getLocalizedMessage(), -1, x));
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -194,8 +194,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||||
invokeErrorCallback(err);
|
invokeErrorCallback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onError(String msg, int httpStatus){
|
void onError(String msg, int httpStatus, Throwable exception){
|
||||||
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus));
|
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus, exception));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSuccess(T resp){
|
void onSuccess(T resp){
|
||||||
|
|
|
@ -7,8 +7,8 @@ import java.util.Map;
|
||||||
public class MastodonDetailedErrorResponse extends MastodonErrorResponse{
|
public class MastodonDetailedErrorResponse extends MastodonErrorResponse{
|
||||||
public Map<String, List<FieldError>> detailedErrors;
|
public Map<String, List<FieldError>> detailedErrors;
|
||||||
|
|
||||||
public MastodonDetailedErrorResponse(String error, int httpStatus){
|
public MastodonDetailedErrorResponse(String error, int httpStatus, Throwable exception){
|
||||||
super(error, httpStatus);
|
super(error, httpStatus, exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class FieldError{
|
public static class FieldError{
|
||||||
|
|
|
@ -12,10 +12,12 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||||
public class MastodonErrorResponse extends ErrorResponse{
|
public class MastodonErrorResponse extends ErrorResponse{
|
||||||
public final String error;
|
public final String error;
|
||||||
public final int httpStatus;
|
public final int httpStatus;
|
||||||
|
public final Throwable underlyingException;
|
||||||
|
|
||||||
public MastodonErrorResponse(String error, int httpStatus){
|
public MastodonErrorResponse(String error, int httpStatus, Throwable exception){
|
||||||
this.error=error;
|
this.error=error;
|
||||||
this.httpStatus=httpStatus;
|
this.httpStatus=httpStatus;
|
||||||
|
this.underlyingException=exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.joinmastodon.android.api.requests.statuses;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Attachment;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class GetAttachmentByID extends MastodonAPIRequest<Attachment>{
|
||||||
|
public GetAttachmentByID(String id){
|
||||||
|
super(HttpMethod.GET, "/media/"+id, Attachment.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateAndPostprocessResponse(Attachment respObj, Response httpResponse) throws IOException{
|
||||||
|
if(httpResponse.code()==206)
|
||||||
|
respObj.url="";
|
||||||
|
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import java.io.IOException;
|
||||||
|
|
||||||
import okhttp3.MultipartBody;
|
import okhttp3.MultipartBody;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
public class UploadAttachment extends MastodonAPIRequest<Attachment>{
|
public class UploadAttachment extends MastodonAPIRequest<Attachment>{
|
||||||
private Uri uri;
|
private Uri uri;
|
||||||
|
@ -40,6 +41,18 @@ public class UploadAttachment extends MastodonAPIRequest<Attachment>{
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getPathPrefix(){
|
||||||
|
return "/api/v2";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateAndPostprocessResponse(Attachment respObj, Response httpResponse) throws IOException{
|
||||||
|
if(respObj.url==null)
|
||||||
|
respObj.url="";
|
||||||
|
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RequestBody getRequestBody() throws IOException{
|
public RequestBody getRequestBody() throws IOException{
|
||||||
MultipartBody.Builder builder=new MultipartBody.Builder()
|
MultipartBody.Builder builder=new MultipartBody.Builder()
|
||||||
|
|
|
@ -4,13 +4,18 @@ import android.animation.ObjectAnimator;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Outline;
|
import android.graphics.Outline;
|
||||||
import android.graphics.PixelFormat;
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.RenderEffect;
|
||||||
|
import android.graphics.Shader;
|
||||||
import android.graphics.drawable.LayerDrawable;
|
import android.graphics.drawable.LayerDrawable;
|
||||||
import android.icu.text.BreakIterator;
|
import android.icu.text.BreakIterator;
|
||||||
|
import android.media.MediaMetadataRetriever;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -51,9 +56,12 @@ import com.twitter.twittertext.TwitterTextEmojiRegex;
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
|
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||||
import org.joinmastodon.android.api.ProgressListener;
|
import org.joinmastodon.android.api.ProgressListener;
|
||||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||||
import org.joinmastodon.android.api.requests.statuses.EditStatus;
|
import org.joinmastodon.android.api.requests.statuses.EditStatus;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.GetAttachmentByID;
|
||||||
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
|
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
@ -78,6 +86,7 @@ import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan;
|
||||||
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
|
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||||
|
import org.joinmastodon.android.ui.utils.TransferSpeedTracker;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.ComposeEditText;
|
import org.joinmastodon.android.ui.views.ComposeEditText;
|
||||||
import org.joinmastodon.android.ui.views.ComposeMediaLayout;
|
import org.joinmastodon.android.ui.views.ComposeMediaLayout;
|
||||||
|
@ -86,6 +95,9 @@ import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
|
||||||
import org.parceler.Parcel;
|
import org.parceler.Parcel;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -107,6 +119,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
private static final int MEDIA_RESULT=717;
|
private static final int MEDIA_RESULT=717;
|
||||||
private static final int IMAGE_DESCRIPTION_RESULT=363;
|
private static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||||
private static final int MAX_ATTACHMENTS=4;
|
private static final int MAX_ATTACHMENTS=4;
|
||||||
|
private static final String TAG="ComposeFragment";
|
||||||
|
|
||||||
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
|
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
@ -154,8 +167,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
|
|
||||||
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
|
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
|
||||||
|
|
||||||
private ArrayList<DraftMediaAttachment> queuedAttachments=new ArrayList<>(), failedAttachments=new ArrayList<>(), attachments=new ArrayList<>(), allAttachments=new ArrayList<>();
|
private ArrayList<DraftMediaAttachment> attachments=new ArrayList<>();
|
||||||
private DraftMediaAttachment uploadingAttachment;
|
|
||||||
|
|
||||||
private List<EmojiCategory> customEmojis;
|
private List<EmojiCategory> customEmojis;
|
||||||
private CustomEmojiPopupKeyboard emojiKeyboard;
|
private CustomEmojiPopupKeyboard emojiKeyboard;
|
||||||
|
@ -181,6 +193,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
private boolean pollChanged;
|
private boolean pollChanged;
|
||||||
private boolean creatingView;
|
private boolean creatingView;
|
||||||
private boolean ignoreSelectionChanges=false;
|
private boolean ignoreSelectionChanges=false;
|
||||||
|
private Runnable updateUploadEtaRunnable;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
@ -223,8 +236,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy(){
|
public void onDestroy(){
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if(uploadingAttachment!=null && uploadingAttachment.uploadRequest!=null)
|
for(DraftMediaAttachment att:attachments){
|
||||||
uploadingAttachment.uploadRequest.cancel();
|
if(att.isUploadingOrProcessing())
|
||||||
|
att.cancelUpload();
|
||||||
|
}
|
||||||
|
if(updateUploadEtaRunnable!=null){
|
||||||
|
UiUtils.removeCallbacks(updateUploadEtaRunnable);
|
||||||
|
updateUploadEtaRunnable=null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -344,9 +363,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
attachments.add(att);
|
attachments.add(att);
|
||||||
}
|
}
|
||||||
attachmentsView.setVisibility(View.VISIBLE);
|
attachmentsView.setVisibility(View.VISIBLE);
|
||||||
}else if(!allAttachments.isEmpty()){
|
}else if(!attachments.isEmpty()){
|
||||||
attachmentsView.setVisibility(View.VISIBLE);
|
attachmentsView.setVisibility(View.VISIBLE);
|
||||||
for(DraftMediaAttachment att:allAttachments){
|
for(DraftMediaAttachment att:attachments){
|
||||||
attachmentsView.addView(createMediaAttachmentView(att));
|
attachmentsView.addView(createMediaAttachmentView(att));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -611,8 +630,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
}
|
}
|
||||||
if(publishButton==null)
|
if(publishButton==null)
|
||||||
return;
|
return;
|
||||||
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty()
|
int nonDoneAttachmentCount=0;
|
||||||
&& (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
for(DraftMediaAttachment att:attachments){
|
||||||
|
if(att.state!=AttachmentUploadState.DONE)
|
||||||
|
nonDoneAttachmentCount++;
|
||||||
|
}
|
||||||
|
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onCustomEmojiClick(Emoji emoji){
|
private void onCustomEmojiClick(Emoji emoji){
|
||||||
|
@ -719,8 +742,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
boolean pollFieldsHaveContent=false;
|
boolean pollFieldsHaveContent=false;
|
||||||
for(DraftPollOption opt:pollOptions)
|
for(DraftPollOption opt:pollOptions)
|
||||||
pollFieldsHaveContent|=opt.edit.length()>0;
|
pollFieldsHaveContent|=opt.edit.length()>0;
|
||||||
return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !attachments.isEmpty()
|
return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !attachments.isEmpty() || pollFieldsHaveContent;
|
||||||
|| uploadingAttachment!=null || !queuedAttachments.isEmpty() || !failedAttachments.isEmpty() || pollFieldsHaveContent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -821,7 +843,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
}
|
}
|
||||||
if(size>sizeLimit){
|
if(size>sizeLimit){
|
||||||
float mb=sizeLimit/(float) (1024*1024);
|
float mb=sizeLimit/(float) (1024*1024);
|
||||||
String sMb=String.format(Locale.getDefault(), mb%1f==0f ? "%f" : "%.2f", mb);
|
String sMb=String.format(Locale.getDefault(), mb%1f==0f ? "%.0f" : "%.2f", mb);
|
||||||
showMediaAttachmentError(getString(R.string.media_attachment_too_big, UiUtils.getFileName(uri), sMb));
|
showMediaAttachmentError(getString(R.string.media_attachment_too_big, UiUtils.getFileName(uri), sMb));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -830,18 +852,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
pollBtn.setEnabled(false);
|
pollBtn.setEnabled(false);
|
||||||
DraftMediaAttachment draft=new DraftMediaAttachment();
|
DraftMediaAttachment draft=new DraftMediaAttachment();
|
||||||
draft.uri=uri;
|
draft.uri=uri;
|
||||||
|
draft.mimeType=type;
|
||||||
draft.description=description;
|
draft.description=description;
|
||||||
|
|
||||||
attachmentsView.addView(createMediaAttachmentView(draft));
|
attachmentsView.addView(createMediaAttachmentView(draft));
|
||||||
allAttachments.add(draft);
|
attachments.add(draft);
|
||||||
attachmentsView.setVisibility(View.VISIBLE);
|
attachmentsView.setVisibility(View.VISIBLE);
|
||||||
draft.overlay.setVisibility(View.VISIBLE);
|
draft.setOverlayVisible(true, false);
|
||||||
draft.infoBar.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
if(uploadingAttachment==null){
|
if(!areThereAnyUploadingAttachments()){
|
||||||
uploadMediaAttachment(draft);
|
uploadNextQueuedAttachment();
|
||||||
}else{
|
|
||||||
queuedAttachments.add(draft);
|
|
||||||
}
|
}
|
||||||
updatePublishButtonState();
|
updatePublishButtonState();
|
||||||
if(getMediaAttachmentsCount()==MAX_ATTACHMENTS)
|
if(getMediaAttachmentsCount()==MAX_ATTACHMENTS)
|
||||||
|
@ -860,25 +880,31 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
private View createMediaAttachmentView(DraftMediaAttachment draft){
|
private View createMediaAttachmentView(DraftMediaAttachment draft){
|
||||||
View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
|
View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
|
||||||
ImageView img=thumb.findViewById(R.id.thumb);
|
ImageView img=thumb.findViewById(R.id.thumb);
|
||||||
|
if(draft.mimeType.startsWith("image/")){
|
||||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(draft.uri, V.dp(250), V.dp(250)));
|
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(draft.uri, V.dp(250), V.dp(250)));
|
||||||
|
}else if(draft.mimeType.startsWith("video/")){
|
||||||
|
loadVideoThumbIntoView(img, draft.uri);
|
||||||
|
}
|
||||||
TextView fileName=thumb.findViewById(R.id.file_name);
|
TextView fileName=thumb.findViewById(R.id.file_name);
|
||||||
fileName.setText(UiUtils.getFileName(draft.uri));
|
fileName.setText(UiUtils.getFileName(draft.uri));
|
||||||
|
|
||||||
draft.view=thumb;
|
draft.view=thumb;
|
||||||
|
draft.imageView=img;
|
||||||
draft.progressBar=thumb.findViewById(R.id.progress);
|
draft.progressBar=thumb.findViewById(R.id.progress);
|
||||||
draft.infoBar=thumb.findViewById(R.id.info_bar);
|
draft.infoBar=thumb.findViewById(R.id.info_bar);
|
||||||
draft.overlay=thumb.findViewById(R.id.overlay);
|
draft.overlay=thumb.findViewById(R.id.overlay);
|
||||||
draft.descriptionView=thumb.findViewById(R.id.description);
|
draft.descriptionView=thumb.findViewById(R.id.description);
|
||||||
|
draft.uploadStateTitle=thumb.findViewById(R.id.state_title);
|
||||||
|
draft.uploadStateText=thumb.findViewById(R.id.state_text);
|
||||||
ImageButton btn=thumb.findViewById(R.id.remove_btn);
|
ImageButton btn=thumb.findViewById(R.id.remove_btn);
|
||||||
btn.setTag(draft);
|
btn.setTag(draft);
|
||||||
btn.setOnClickListener(this::onRemoveMediaAttachmentClick);
|
btn.setOnClickListener(this::onRemoveMediaAttachmentClick);
|
||||||
btn=thumb.findViewById(R.id.remove_btn2);
|
btn=thumb.findViewById(R.id.remove_btn2);
|
||||||
btn.setTag(draft);
|
btn.setTag(draft);
|
||||||
btn.setOnClickListener(this::onRemoveMediaAttachmentClick);
|
btn.setOnClickListener(this::onRemoveMediaAttachmentClick);
|
||||||
Button retry=thumb.findViewById(R.id.retry_upload);
|
ImageButton retry=thumb.findViewById(R.id.retry_or_cancel_upload);
|
||||||
retry.setTag(draft);
|
retry.setTag(draft);
|
||||||
retry.setOnClickListener(this::onRetryMediaUploadClick);
|
retry.setOnClickListener(this::onRetryOrCancelMediaUploadClick);
|
||||||
retry.setVisibility(View.GONE);
|
|
||||||
draft.retryButton=retry;
|
draft.retryButton=retry;
|
||||||
draft.infoBar.setTag(draft);
|
draft.infoBar.setTag(draft);
|
||||||
draft.infoBar.setOnClickListener(this::onEditMediaDescriptionClick);
|
draft.infoBar.setOnClickListener(this::onEditMediaDescriptionClick);
|
||||||
|
@ -886,12 +912,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
if(!TextUtils.isEmpty(draft.description))
|
if(!TextUtils.isEmpty(draft.description))
|
||||||
draft.descriptionView.setText(draft.description);
|
draft.descriptionView.setText(draft.description);
|
||||||
|
|
||||||
if(uploadingAttachment!=draft && !queuedAttachments.contains(draft)){
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){
|
||||||
draft.progressBar.setVisibility(View.GONE);
|
draft.overlay.setBackgroundColor(0xA6000000);
|
||||||
}
|
}
|
||||||
if(failedAttachments.contains(draft)){
|
|
||||||
draft.infoBar.setVisibility(View.GONE);
|
if(draft.state==AttachmentUploadState.UPLOADING || draft.state==AttachmentUploadState.PROCESSING || draft.state==AttachmentUploadState.QUEUED){
|
||||||
draft.overlay.setVisibility(View.VISIBLE);
|
draft.progressBar.setVisibility(View.GONE);
|
||||||
|
}else if(draft.state==AttachmentUploadState.ERROR){
|
||||||
|
draft.setOverlayVisible(true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return thumb;
|
return thumb;
|
||||||
|
@ -903,67 +931,92 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
draft.uri=uri;
|
draft.uri=uri;
|
||||||
draft.description=description;
|
draft.description=description;
|
||||||
attachmentsView.addView(createMediaAttachmentView(draft));
|
attachmentsView.addView(createMediaAttachmentView(draft));
|
||||||
allAttachments.add(draft);
|
attachments.add(draft);
|
||||||
attachmentsView.setVisibility(View.VISIBLE);
|
attachmentsView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void uploadMediaAttachment(DraftMediaAttachment attachment){
|
private void uploadMediaAttachment(DraftMediaAttachment attachment){
|
||||||
if(uploadingAttachment!=null)
|
if(areThereAnyUploadingAttachments()){
|
||||||
throw new IllegalStateException("there is already an attachment being uploaded");
|
throw new IllegalStateException("there is already an attachment being uploaded");
|
||||||
uploadingAttachment=attachment;
|
}
|
||||||
|
attachment.state=AttachmentUploadState.UPLOADING;
|
||||||
attachment.progressBar.setVisibility(View.VISIBLE);
|
attachment.progressBar.setVisibility(View.VISIBLE);
|
||||||
ObjectAnimator rotationAnimator=ObjectAnimator.ofFloat(attachment.progressBar, View.ROTATION, 0f, 360f);
|
ObjectAnimator rotationAnimator=ObjectAnimator.ofFloat(attachment.progressBar, View.ROTATION, 0f, 360f);
|
||||||
rotationAnimator.setInterpolator(new LinearInterpolator());
|
rotationAnimator.setInterpolator(new LinearInterpolator());
|
||||||
rotationAnimator.setDuration(1500);
|
rotationAnimator.setDuration(1500);
|
||||||
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
||||||
rotationAnimator.start();
|
rotationAnimator.start();
|
||||||
|
attachment.progressBarAnimator=rotationAnimator;
|
||||||
int maxSize=0;
|
int maxSize=0;
|
||||||
String contentType=getActivity().getContentResolver().getType(attachment.uri);
|
String contentType=getActivity().getContentResolver().getType(attachment.uri);
|
||||||
if(contentType!=null && contentType.startsWith("image/")){
|
if(contentType!=null && contentType.startsWith("image/")){
|
||||||
maxSize=2_073_600; // TODO get this from instance configuration when it gets added there
|
maxSize=2_073_600; // TODO get this from instance configuration when it gets added there
|
||||||
}
|
}
|
||||||
|
attachment.uploadStateTitle.setText("");
|
||||||
|
attachment.uploadStateText.setText("");
|
||||||
|
attachment.progressBar.setProgress(0);
|
||||||
|
attachment.speedTracker.reset();
|
||||||
|
attachment.speedTracker.addSample(0);
|
||||||
attachment.uploadRequest=(UploadAttachment) new UploadAttachment(attachment.uri, maxSize, attachment.description)
|
attachment.uploadRequest=(UploadAttachment) new UploadAttachment(attachment.uri, maxSize, attachment.description)
|
||||||
.setProgressListener(new ProgressListener(){
|
.setProgressListener(new ProgressListener(){
|
||||||
@Override
|
@Override
|
||||||
public void onProgress(long transferred, long total){
|
public void onProgress(long transferred, long total){
|
||||||
|
if(updateUploadEtaRunnable==null){
|
||||||
|
UiUtils.runOnUiThread(updateUploadEtaRunnable=ComposeFragment.this::updateUploadETAs, 100);
|
||||||
|
}
|
||||||
int progress=Math.round(transferred/(float)total*attachment.progressBar.getMax());
|
int progress=Math.round(transferred/(float)total*attachment.progressBar.getMax());
|
||||||
if(Build.VERSION.SDK_INT>=24)
|
if(Build.VERSION.SDK_INT>=24)
|
||||||
attachment.progressBar.setProgress(progress, true);
|
attachment.progressBar.setProgress(progress, true);
|
||||||
else
|
else
|
||||||
attachment.progressBar.setProgress(progress);
|
attachment.progressBar.setProgress(progress);
|
||||||
|
|
||||||
|
attachment.speedTracker.setTotalBytes(total);
|
||||||
|
attachment.uploadStateTitle.setText(getString(R.string.file_upload_progress, UiUtils.formatFileSize(getActivity(), transferred, true), UiUtils.formatFileSize(getActivity(), total, true)));
|
||||||
|
attachment.speedTracker.addSample(transferred);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Attachment result){
|
public void onSuccess(Attachment result){
|
||||||
attachment.serverAttachment=result;
|
attachment.serverAttachment=result;
|
||||||
attachment.uploadRequest=null;
|
if(TextUtils.isEmpty(result.url)){
|
||||||
uploadingAttachment=null;
|
attachment.state=AttachmentUploadState.PROCESSING;
|
||||||
attachments.add(attachment);
|
attachment.processingPollingRunnable=()->pollForMediaAttachmentProcessing(attachment);
|
||||||
attachment.progressBar.setVisibility(View.GONE);
|
if(getActivity()==null)
|
||||||
if(!queuedAttachments.isEmpty())
|
return;
|
||||||
uploadMediaAttachment(queuedAttachments.remove(0));
|
attachment.uploadStateTitle.setText(R.string.upload_processing);
|
||||||
updatePublishButtonState();
|
attachment.uploadStateText.setText("");
|
||||||
|
UiUtils.runOnUiThread(attachment.processingPollingRunnable, 1000);
|
||||||
rotationAnimator.cancel();
|
if(!areThereAnyUploadingAttachments())
|
||||||
V.setVisibilityAnimated(attachment.overlay, View.GONE);
|
uploadNextQueuedAttachment();
|
||||||
V.setVisibilityAnimated(attachment.infoBar, View.VISIBLE);
|
}else{
|
||||||
|
finishMediaAttachmentUpload(attachment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error){
|
public void onError(ErrorResponse error){
|
||||||
attachment.uploadRequest=null;
|
attachment.uploadRequest=null;
|
||||||
uploadingAttachment=null;
|
attachment.progressBarAnimator=null;
|
||||||
failedAttachments.add(attachment);
|
attachment.state=AttachmentUploadState.ERROR;
|
||||||
// error.showToast(getActivity());
|
attachment.uploadStateTitle.setText(R.string.upload_failed);
|
||||||
Toast.makeText(getActivity(), R.string.image_upload_failed, Toast.LENGTH_SHORT).show();
|
if(error instanceof MastodonErrorResponse er){
|
||||||
|
if(er.underlyingException instanceof SocketException || er.underlyingException instanceof UnknownHostException || er.underlyingException instanceof InterruptedIOException)
|
||||||
|
attachment.uploadStateText.setText(R.string.upload_error_connection_lost);
|
||||||
|
else
|
||||||
|
attachment.uploadStateText.setText(er.error);
|
||||||
|
}else{
|
||||||
|
attachment.uploadStateText.setText("");
|
||||||
|
}
|
||||||
|
attachment.retryButton.setImageResource(R.drawable.ic_fluent_arrow_clockwise_24_filled);
|
||||||
|
attachment.retryButton.setContentDescription(getString(R.string.retry_upload));
|
||||||
|
|
||||||
rotationAnimator.cancel();
|
rotationAnimator.cancel();
|
||||||
V.setVisibilityAnimated(attachment.retryButton, View.VISIBLE);
|
V.setVisibilityAnimated(attachment.retryButton, View.VISIBLE);
|
||||||
V.setVisibilityAnimated(attachment.progressBar, View.GONE);
|
V.setVisibilityAnimated(attachment.progressBar, View.GONE);
|
||||||
|
|
||||||
if(!queuedAttachments.isEmpty())
|
if(!areThereAnyUploadingAttachments())
|
||||||
uploadMediaAttachment(queuedAttachments.remove(0));
|
uploadNextQueuedAttachment();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
|
@ -971,35 +1024,107 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
|
|
||||||
private void onRemoveMediaAttachmentClick(View v){
|
private void onRemoveMediaAttachmentClick(View v){
|
||||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||||
if(att==uploadingAttachment){
|
if(att.isUploadingOrProcessing())
|
||||||
att.uploadRequest.cancel();
|
att.cancelUpload();
|
||||||
uploadingAttachment=null;
|
|
||||||
if(!queuedAttachments.isEmpty())
|
|
||||||
uploadMediaAttachment(queuedAttachments.remove(0));
|
|
||||||
}else{
|
|
||||||
attachments.remove(att);
|
attachments.remove(att);
|
||||||
queuedAttachments.remove(att);
|
uploadNextQueuedAttachment();
|
||||||
failedAttachments.remove(att);
|
|
||||||
}
|
|
||||||
allAttachments.remove(att);
|
|
||||||
attachmentsView.removeView(att.view);
|
attachmentsView.removeView(att.view);
|
||||||
if(getMediaAttachmentsCount()==0)
|
if(getMediaAttachmentsCount()==0)
|
||||||
attachmentsView.setVisibility(View.GONE);
|
attachmentsView.setVisibility(View.GONE);
|
||||||
updatePublishButtonState();
|
updatePublishButtonState();
|
||||||
pollBtn.setEnabled(attachments.isEmpty() && queuedAttachments.isEmpty() && failedAttachments.isEmpty() && uploadingAttachment==null);
|
pollBtn.setEnabled(attachments.isEmpty());
|
||||||
mediaBtn.setEnabled(true);
|
mediaBtn.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onRetryMediaUploadClick(View v){
|
private void onRetryOrCancelMediaUploadClick(View v){
|
||||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||||
if(failedAttachments.remove(att)){
|
if(att.state==AttachmentUploadState.ERROR){
|
||||||
V.setVisibilityAnimated(att.retryButton, View.GONE);
|
att.retryButton.setImageResource(R.drawable.ic_fluent_dismiss_24_filled);
|
||||||
|
att.retryButton.setContentDescription(getString(R.string.cancel));
|
||||||
V.setVisibilityAnimated(att.progressBar, View.VISIBLE);
|
V.setVisibilityAnimated(att.progressBar, View.VISIBLE);
|
||||||
if(uploadingAttachment==null)
|
att.state=AttachmentUploadState.QUEUED;
|
||||||
uploadMediaAttachment(att);
|
if(!areThereAnyUploadingAttachments()){
|
||||||
else
|
uploadNextQueuedAttachment();
|
||||||
queuedAttachments.add(att);
|
|
||||||
}
|
}
|
||||||
|
}else{
|
||||||
|
onRemoveMediaAttachmentClick(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pollForMediaAttachmentProcessing(DraftMediaAttachment attachment){
|
||||||
|
attachment.processingPollingRequest=(GetAttachmentByID) new GetAttachmentByID(attachment.serverAttachment.id)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Attachment result){
|
||||||
|
attachment.processingPollingRequest=null;
|
||||||
|
if(!TextUtils.isEmpty(result.url)){
|
||||||
|
attachment.processingPollingRunnable=null;
|
||||||
|
attachment.serverAttachment=result;
|
||||||
|
finishMediaAttachmentUpload(attachment);
|
||||||
|
}else if(getActivity()!=null){
|
||||||
|
UiUtils.runOnUiThread(attachment.processingPollingRunnable, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
attachment.processingPollingRequest=null;
|
||||||
|
if(getActivity()!=null)
|
||||||
|
UiUtils.runOnUiThread(attachment.processingPollingRunnable, 1000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishMediaAttachmentUpload(DraftMediaAttachment attachment){
|
||||||
|
if(attachment.state!=AttachmentUploadState.PROCESSING && attachment.state!=AttachmentUploadState.UPLOADING)
|
||||||
|
throw new IllegalStateException("Unexpected state "+attachment.state);
|
||||||
|
attachment.uploadRequest=null;
|
||||||
|
attachment.state=AttachmentUploadState.DONE;
|
||||||
|
attachment.progressBar.setVisibility(View.GONE);
|
||||||
|
if(!areThereAnyUploadingAttachments())
|
||||||
|
uploadNextQueuedAttachment();
|
||||||
|
updatePublishButtonState();
|
||||||
|
|
||||||
|
if(attachment.progressBarAnimator!=null){
|
||||||
|
attachment.progressBarAnimator.cancel();
|
||||||
|
attachment.progressBarAnimator=null;
|
||||||
|
}
|
||||||
|
attachment.setOverlayVisible(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void uploadNextQueuedAttachment(){
|
||||||
|
for(DraftMediaAttachment att:attachments){
|
||||||
|
if(att.state==AttachmentUploadState.QUEUED){
|
||||||
|
uploadMediaAttachment(att);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean areThereAnyUploadingAttachments(){
|
||||||
|
for(DraftMediaAttachment att:attachments){
|
||||||
|
if(att.state==AttachmentUploadState.UPLOADING)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUploadETAs(){
|
||||||
|
if(!areThereAnyUploadingAttachments()){
|
||||||
|
UiUtils.removeCallbacks(updateUploadEtaRunnable);
|
||||||
|
updateUploadEtaRunnable=null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for(DraftMediaAttachment att:attachments){
|
||||||
|
if(att.state==AttachmentUploadState.UPLOADING){
|
||||||
|
long eta=att.speedTracker.updateAndGetETA();
|
||||||
|
// Log.i(TAG, "onProgress: transfer speed "+UiUtils.formatFileSize(getActivity(), Math.round(att.speedTracker.getLastSpeed()), false)+" average "+UiUtils.formatFileSize(getActivity(), Math.round(att.speedTracker.getAverageSpeed()), false)+" eta "+eta);
|
||||||
|
String time=String.format("%d:%02d", eta/60, eta%60);
|
||||||
|
att.uploadStateText.setText(getString(R.string.file_upload_time_remaining, time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UiUtils.runOnUiThread(updateUploadEtaRunnable, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onEditMediaDescriptionClick(View v){
|
private void onEditMediaDescriptionClick(View v){
|
||||||
|
@ -1114,7 +1239,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getMediaAttachmentsCount(){
|
private int getMediaAttachmentsCount(){
|
||||||
return allAttachments.size();
|
return attachments.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onVisibilityClick(View v){
|
private void onVisibilityClick(View v){
|
||||||
|
@ -1233,6 +1358,30 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
finishAutocomplete();
|
finishAutocomplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadVideoThumbIntoView(ImageView target, Uri uri){
|
||||||
|
MastodonAPIController.runInBackground(()->{
|
||||||
|
Context context=getActivity();
|
||||||
|
if(context==null)
|
||||||
|
return;
|
||||||
|
try{
|
||||||
|
MediaMetadataRetriever mmr=new MediaMetadataRetriever();
|
||||||
|
mmr.setDataSource(context, uri);
|
||||||
|
Bitmap frame=mmr.getFrameAtTime(3_000_000);
|
||||||
|
mmr.release();
|
||||||
|
int size=Math.max(frame.getWidth(), frame.getHeight());
|
||||||
|
int maxSize=V.dp(250);
|
||||||
|
if(size>maxSize){
|
||||||
|
float factor=maxSize/(float)size;
|
||||||
|
frame=Bitmap.createScaledBitmap(frame, Math.round(frame.getWidth()*factor), Math.round(frame.getHeight()*factor), true);
|
||||||
|
}
|
||||||
|
Bitmap finalFrame=frame;
|
||||||
|
target.post(()->target.setImageBitmap(finalFrame));
|
||||||
|
}catch(Exception x){
|
||||||
|
Log.w(TAG, "loadVideoThumbIntoView: error getting video frame", x);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CharSequence getTitle(){
|
public CharSequence getTitle(){
|
||||||
return getString(R.string.new_post);
|
return getString(R.string.new_post);
|
||||||
|
@ -1253,14 +1402,75 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||||
public Attachment serverAttachment;
|
public Attachment serverAttachment;
|
||||||
public Uri uri;
|
public Uri uri;
|
||||||
public transient UploadAttachment uploadRequest;
|
public transient UploadAttachment uploadRequest;
|
||||||
|
public transient GetAttachmentByID processingPollingRequest;
|
||||||
public String description;
|
public String description;
|
||||||
|
public String mimeType;
|
||||||
|
public AttachmentUploadState state=AttachmentUploadState.QUEUED;
|
||||||
|
|
||||||
public transient View view;
|
public transient View view;
|
||||||
public transient ProgressBar progressBar;
|
public transient ProgressBar progressBar;
|
||||||
public transient TextView descriptionView;
|
public transient TextView descriptionView;
|
||||||
public transient View overlay;
|
public transient View overlay;
|
||||||
public transient View infoBar;
|
public transient View infoBar;
|
||||||
public transient Button retryButton;
|
public transient ImageButton retryButton;
|
||||||
|
public transient ObjectAnimator progressBarAnimator;
|
||||||
|
public transient Runnable processingPollingRunnable;
|
||||||
|
public transient ImageView imageView;
|
||||||
|
public transient TextView uploadStateTitle, uploadStateText;
|
||||||
|
public transient TransferSpeedTracker speedTracker=new TransferSpeedTracker();
|
||||||
|
|
||||||
|
public void cancelUpload(){
|
||||||
|
switch(state){
|
||||||
|
case UPLOADING -> {
|
||||||
|
if(uploadRequest!=null){
|
||||||
|
uploadRequest.cancel();
|
||||||
|
uploadRequest=null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PROCESSING -> {
|
||||||
|
if(processingPollingRunnable!=null){
|
||||||
|
UiUtils.removeCallbacks(processingPollingRunnable);
|
||||||
|
processingPollingRunnable=null;
|
||||||
|
}
|
||||||
|
if(processingPollingRequest!=null){
|
||||||
|
processingPollingRequest.cancel();
|
||||||
|
processingPollingRequest=null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> throw new IllegalStateException("Unexpected state "+state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUploadingOrProcessing(){
|
||||||
|
return state==AttachmentUploadState.UPLOADING || state==AttachmentUploadState.PROCESSING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOverlayVisible(boolean visible, boolean animated){
|
||||||
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){
|
||||||
|
if(visible){
|
||||||
|
imageView.setRenderEffect(RenderEffect.createBlurEffect(V.dp(16), V.dp(16), Shader.TileMode.REPEAT));
|
||||||
|
}else{
|
||||||
|
imageView.setRenderEffect(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int infoBarVis=visible ? View.GONE : View.VISIBLE;
|
||||||
|
int overlayVis=visible ? View.VISIBLE : View.GONE;
|
||||||
|
if(animated){
|
||||||
|
V.setVisibilityAnimated(infoBar, infoBarVis);
|
||||||
|
V.setVisibilityAnimated(overlay, overlayVis);
|
||||||
|
}else{
|
||||||
|
infoBar.setVisibility(infoBarVis);
|
||||||
|
overlay.setVisibility(overlayVis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AttachmentUploadState{
|
||||||
|
QUEUED,
|
||||||
|
UPLOADING,
|
||||||
|
PROCESSING,
|
||||||
|
ERROR,
|
||||||
|
DONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class DraftPollOption{
|
private static class DraftPollOption{
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package org.joinmastodon.android.ui.utils;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
|
public class TransferSpeedTracker{
|
||||||
|
private final double SMOOTHING_FACTOR=0.05;
|
||||||
|
|
||||||
|
private long lastKnownPos;
|
||||||
|
private long lastKnownPosTime;
|
||||||
|
private double lastSpeed;
|
||||||
|
private double averageSpeed;
|
||||||
|
private long totalBytes;
|
||||||
|
|
||||||
|
public void addSample(long position){
|
||||||
|
if(lastKnownPosTime==0){
|
||||||
|
lastKnownPosTime=SystemClock.uptimeMillis();
|
||||||
|
lastKnownPos=position;
|
||||||
|
}else{
|
||||||
|
long time=SystemClock.uptimeMillis();
|
||||||
|
lastSpeed=(position-lastKnownPos)/((double)(time-lastKnownPosTime)/1000.0);
|
||||||
|
lastKnownPos=position;
|
||||||
|
lastKnownPosTime=time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLastSpeed(){
|
||||||
|
return lastSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAverageSpeed(){
|
||||||
|
return averageSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long updateAndGetETA(){ // must be called at a constant interval
|
||||||
|
if(averageSpeed==0.0)
|
||||||
|
averageSpeed=lastSpeed;
|
||||||
|
else
|
||||||
|
averageSpeed=SMOOTHING_FACTOR*lastSpeed+(1.0-SMOOTHING_FACTOR)*averageSpeed;
|
||||||
|
return Math.round((totalBytes-lastKnownPos)/averageSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalBytes(long totalBytes){
|
||||||
|
this.totalBytes=totalBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset(){
|
||||||
|
lastKnownPos=lastKnownPosTime=0;
|
||||||
|
lastSpeed=averageSpeed=0.0;
|
||||||
|
totalBytes=0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -201,6 +201,14 @@ public class UiUtils{
|
||||||
mainHandler.post(runnable);
|
mainHandler.post(runnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void runOnUiThread(Runnable runnable, long delay){
|
||||||
|
mainHandler.postDelayed(runnable, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeCallbacks(Runnable runnable){
|
||||||
|
mainHandler.removeCallbacks(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
/** Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. */
|
/** Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. */
|
||||||
public static int lerp(int startValue, int endValue, float fraction) {
|
public static int lerp(int startValue, int endValue, float fraction) {
|
||||||
return startValue + Math.round(fraction * (endValue - startValue));
|
return startValue + Math.round(fraction * (endValue - startValue));
|
||||||
|
@ -218,6 +226,18 @@ public class UiUtils{
|
||||||
return uri.getLastPathSegment();
|
return uri.getLastPathSegment();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String formatFileSize(Context context, long size, boolean atLeastKB){
|
||||||
|
if(size<1024 && !atLeastKB){
|
||||||
|
return context.getString(R.string.file_size_bytes, size);
|
||||||
|
}else if(size<1024*1024){
|
||||||
|
return context.getString(R.string.file_size_kb, size/1024.0);
|
||||||
|
}else if(size<1024*1024*1024){
|
||||||
|
return context.getString(R.string.file_size_mb, size/(1024.0*1024.0));
|
||||||
|
}else{
|
||||||
|
return context.getString(R.string.file_size_gb, size/(1024.0*1024.0*1024.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static MediaType getFileMediaType(File file){
|
public static MediaType getFileMediaType(File file){
|
||||||
String name=file.getName();
|
String name=file.getName();
|
||||||
return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.')+1)));
|
return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.')+1)));
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/highlight_over_dark">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@color/gray_600"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:pathData="M12 4.75c-4.004 0-7.25 3.246-7.25 7.25s3.246 7.25 7.25 7.25 7.25-3.246 7.25-7.25c0-0.286-0.017-0.567-0.049-0.844C19.133 10.568 19.56 10 20.151 10c0.515 0 0.968 0.358 1.03 0.87 0.046 0.37 0.069 0.747 0.069 1.13 0 5.109-4.141 9.25-9.25 9.25S2.75 17.109 2.75 12 6.891 2.75 12 2.75c2.173 0 4.171 0.75 5.75 2.004V4.25c0-0.552 0.448-1 1-1s1 0.448 1 1v2.698L19.784 7H19.75v0.25c0 0.552-0.448 1-1 1h-3c-0.552 0-1-0.448-1-1s0.448-1 1-1h0.666c-1.222-0.94-2.754-1.5-4.416-1.5z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:pathData="M4.21 4.387l0.083-0.094c0.36-0.36 0.928-0.388 1.32-0.083l0.094 0.083L12 10.585l6.293-6.292c0.39-0.39 1.024-0.39 1.414 0 0.39 0.39 0.39 1.024 0 1.414L13.415 12l6.292 6.293c0.36 0.36 0.388 0.928 0.083 1.32l-0.083 0.094c-0.36 0.36-0.928 0.388-1.32 0.083l-0.094-0.083L12 13.415l-6.293 6.292c-0.39 0.39-1.024 0.39-1.414 0-0.39-0.39-0.39-1.024 0-1.414L10.585 12 4.293 5.707c-0.36-0.36-0.388-0.928-0.083-1.32l0.083-0.094L4.21 4.387z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
|
@ -6,7 +6,7 @@
|
||||||
android:shape="ring"
|
android:shape="ring"
|
||||||
android:thickness="4dp"
|
android:thickness="4dp"
|
||||||
android:useLevel="true">
|
android:useLevel="true">
|
||||||
<solid android:color="?android:colorAccent"/>
|
<solid android:color="@color/gray_100"/>
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
|
@ -65,30 +65,68 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#cc000000"
|
android:background="#cc000000"
|
||||||
android:backgroundTint="?colorWindowBackground"
|
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
tools:visibility="visible"
|
tools:visibility="visible"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/retry_or_cancel_upload"
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:src="@drawable/ic_fluent_dismiss_24_filled"
|
||||||
|
android:contentDescription="@string/cancel"
|
||||||
|
android:tint="@color/gray_100"
|
||||||
|
android:background="@drawable/bg_upload_progress"/>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progress"
|
android:id="@+id/progress"
|
||||||
android:layout_width="44dp"
|
android:layout_width="44dp"
|
||||||
android:layout_height="44dp"
|
android:layout_height="44dp"
|
||||||
android:layout_gravity="center"
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
android:progressDrawable="@drawable/upload_progress"
|
android:progressDrawable="@drawable/upload_progress"
|
||||||
android:max="1000"
|
android:max="1000"
|
||||||
android:padding="0dp"
|
android:padding="0dp"
|
||||||
android:indeterminateOnly="false"
|
android:indeterminateOnly="false"
|
||||||
android:indeterminate="false"/>
|
android:indeterminate="false"/>
|
||||||
|
|
||||||
<Button
|
<TextView
|
||||||
android:id="@+id/retry_upload"
|
android:id="@+id/state_title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="16dp"
|
||||||
android:layout_gravity="start|bottom"
|
android:layout_below="@id/retry_or_cancel_upload"
|
||||||
style="?secondaryButtonStyle"
|
android:layout_marginTop="16dp"
|
||||||
android:text="@string/retry_upload"/>
|
android:textColor="@color/gray_200"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
tools:text="Upload failed"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/state_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_below="@id/state_title"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:textColor="@color/gray_200"
|
||||||
|
android:gravity="center_horizontal|top"
|
||||||
|
android:lines="2"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="Your device lost connection to the internet"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/remove_btn2"
|
android:id="@+id/remove_btn2"
|
||||||
|
@ -98,6 +136,7 @@
|
||||||
android:layout_gravity="end|bottom"
|
android:layout_gravity="end|bottom"
|
||||||
android:background="?android:selectableItemBackgroundBorderless"
|
android:background="?android:selectableItemBackgroundBorderless"
|
||||||
android:tint="#D92C2C"
|
android:tint="#D92C2C"
|
||||||
|
android:contentDescription="@string/delete"
|
||||||
android:src="@drawable/ic_fluent_delete_20_regular"/>
|
android:src="@drawable/ic_fluent_delete_20_regular"/>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
|
@ -210,8 +210,6 @@
|
||||||
<string name="content_warning">Content warning</string>
|
<string name="content_warning">Content warning</string>
|
||||||
<string name="add_image_description">Add image description…</string>
|
<string name="add_image_description">Add image description…</string>
|
||||||
<string name="retry_upload">Retry upload</string>
|
<string name="retry_upload">Retry upload</string>
|
||||||
<string name="image_upload_failed">Image failed to upload</string>
|
|
||||||
<string name="video_upload_failed">Video failed to upload</string>
|
|
||||||
<string name="edit_image">Edit image</string>
|
<string name="edit_image">Edit image</string>
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="add_alt_text">Add alt text</string>
|
<string name="add_alt_text">Add alt text</string>
|
||||||
|
@ -370,4 +368,13 @@
|
||||||
<string name="edit_multiple_changed">Post edited</string>
|
<string name="edit_multiple_changed">Post edited</string>
|
||||||
<string name="edit">Edit</string>
|
<string name="edit">Edit</string>
|
||||||
<string name="discard_changes">Discard changes?</string>
|
<string name="discard_changes">Discard changes?</string>
|
||||||
|
<string name="upload_failed">Upload failed</string>
|
||||||
|
<string name="file_size_bytes">%d bytes</string>
|
||||||
|
<string name="file_size_kb">%.2f KB</string>
|
||||||
|
<string name="file_size_mb">%.2f MB</string>
|
||||||
|
<string name="file_size_gb">%.2f GB</string>
|
||||||
|
<string name="file_upload_progress">%1$s of %2$s</string>
|
||||||
|
<string name="file_upload_time_remaining">%s remaining</string>
|
||||||
|
<string name="upload_error_connection_lost">Your device lost connection to the internet</string>
|
||||||
|
<string name="upload_processing">Processing…</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue