Image resizing

This commit is contained in:
Grishka 2022-03-24 12:39:28 +03:00
parent d47eb752a5
commit e087cf03cc
13 changed files with 341 additions and 116 deletions

View File

@ -10,7 +10,7 @@ android {
applicationId "org.joinmastodon.android" applicationId "org.joinmastodon.android"
minSdk 23 minSdk 23
targetSdk 31 targetSdk 31
versionCode 15 versionCode 16
versionName "0.1" versionName "0.1"
} }

View File

@ -2,31 +2,22 @@ package org.joinmastodon.android.api;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.IOException; import java.io.IOException;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.RequestBody;
import okio.Buffer;
import okio.BufferedSink;
import okio.ForwardingSink;
import okio.Okio; import okio.Okio;
import okio.Sink;
import okio.Source; import okio.Source;
public class ContentUriRequestBody extends RequestBody{ public class ContentUriRequestBody extends CountingRequestBody{
private final Uri uri; private final Uri uri;
private final long length;
private ProgressListener progressListener;
public ContentUriRequestBody(Uri uri, ProgressListener progressListener){ public ContentUriRequestBody(Uri uri, ProgressListener progressListener){
super(progressListener);
this.uri=uri; this.uri=uri;
this.progressListener=progressListener;
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){ try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
cursor.moveToFirst(); cursor.moveToFirst();
length=cursor.getInt(0); length=cursor.getInt(0);
@ -39,40 +30,7 @@ public class ContentUriRequestBody extends RequestBody{
} }
@Override @Override
public long contentLength() throws IOException{ protected Source openSource() throws IOException{
return length; return Okio.source(MastodonApp.context.getContentResolver().openInputStream(uri));
}
@Override
public void writeTo(BufferedSink sink) throws IOException{
if(progressListener!=null){
try(Source source=Okio.source(MastodonApp.context.getContentResolver().openInputStream(uri))){
BufferedSink wrappedSink=Okio.buffer(new CountingSink(sink));
wrappedSink.writeAll(source);
wrappedSink.flush();
}
}else{
try(Source source=Okio.source(MastodonApp.context.getContentResolver().openInputStream(uri))){
sink.writeAll(source);
}
}
}
private class CountingSink extends ForwardingSink{
private long bytesWritten=0;
private long lastCallbackTime;
public CountingSink(Sink delegate){
super(delegate);
}
@Override
public void write(Buffer source, long byteCount) throws IOException{
super.write(source, byteCount);
bytesWritten+=byteCount;
if(SystemClock.uptimeMillis()-lastCallbackTime>=100L || bytesWritten==length){
lastCallbackTime=SystemClock.uptimeMillis();
UiUtils.runOnUiThread(()->progressListener.onProgress(bytesWritten, length));
}
}
} }
} }

View File

@ -0,0 +1,39 @@
package org.joinmastodon.android.api;
import java.io.IOException;
import okhttp3.RequestBody;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
abstract class CountingRequestBody extends RequestBody{
protected long length;
protected ProgressListener progressListener;
CountingRequestBody(ProgressListener progressListener){
this.progressListener=progressListener;
}
@Override
public long contentLength() throws IOException{
return length;
}
@Override
public void writeTo(BufferedSink sink) throws IOException{
if(progressListener!=null){
try(Source source=openSource()){
BufferedSink wrappedSink=Okio.buffer(new CountingSink(length, progressListener, sink));
wrappedSink.writeAll(source);
wrappedSink.flush();
}
}else{
try(Source source=openSource()){
sink.writeAll(source);
}
}
}
protected abstract Source openSource() throws IOException;
}

View File

@ -0,0 +1,34 @@
package org.joinmastodon.android.api;
import android.os.SystemClock;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.IOException;
import okio.Buffer;
import okio.ForwardingSink;
import okio.Sink;
class CountingSink extends ForwardingSink{
private long bytesWritten=0;
private long lastCallbackTime;
private final long length;
private final ProgressListener progressListener;
public CountingSink(long length, ProgressListener progressListener, Sink delegate){
super(delegate);
this.length=length;
this.progressListener=progressListener;
}
@Override
public void write(Buffer source, long byteCount) throws IOException{
super.write(source, byteCount);
bytesWritten+=byteCount;
if(SystemClock.uptimeMillis()-lastCallbackTime>=100L || bytesWritten==length){
lastCallbackTime=SystemClock.uptimeMillis();
UiUtils.runOnUiThread(()->progressListener.onProgress(bytesWritten, length));
}
}
}

View File

@ -135,7 +135,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
return method; return method;
} }
public RequestBody getRequestBody(){ public RequestBody getRequestBody() throws IOException{
return requestBody==null ? null : new JsonObjectRequestBody(requestBody); return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
} }

View File

@ -0,0 +1,128 @@
package org.joinmastodon.android.api;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ImageDecoder;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.provider.OpenableColumns;
import org.joinmastodon.android.MastodonApp;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import androidx.annotation.NonNull;
import okhttp3.MediaType;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
public class ResizedImageRequestBody extends CountingRequestBody{
private File tempFile;
private Uri uri;
private String contentType;
public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{
super(progressListener);
this.uri=uri;
contentType=MastodonApp.context.getContentResolver().getType(uri);
BitmapFactory.Options opts=new BitmapFactory.Options();
opts.inJustDecodeBounds=true;
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
BitmapFactory.decodeStream(in, null, opts);
}
if(opts.outWidth*opts.outHeight>maxSize){
Bitmap bitmap;
if(Build.VERSION.SDK_INT>=29){
bitmap=ImageDecoder.decodeBitmap(ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri), (decoder, info, source)->{
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getWidth()/info.getSize().getHeight())));
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getHeight()/info.getSize().getWidth())));
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
decoder.setTargetSize(targetWidth, targetHeight);
});
}else{
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outWidth/opts.outHeight)));
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outHeight/opts.outWidth)));
float factor=opts.outWidth/(float)targetWidth;
opts=new BitmapFactory.Options();
opts.inSampleSize=(int)factor;
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
bitmap=BitmapFactory.decodeStream(in, null, opts);
}
if(factor%1f!=0f){
Bitmap scaled=Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
new Canvas(scaled).drawBitmap(bitmap, null, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), new Paint(Paint.FILTER_BITMAP_FLAG));
bitmap=scaled;
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
int rotation;
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
ExifInterface exif=new ExifInterface(in);
int orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
rotation=switch(orientation){
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
default -> 0;
};
}
if(rotation!=0){
Matrix matrix=new Matrix();
matrix.setRotate(rotation);
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
}
}
}
tempFile=new File(MastodonApp.context.getCacheDir(), "tmp_upload_image");
try(FileOutputStream out=new FileOutputStream(tempFile)){
if("image/png".equals(contentType)){
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
}else{
bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out);
contentType="image/jpeg";
}
}
length=tempFile.length();
}else{
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
cursor.moveToFirst();
length=cursor.getInt(0);
}
}
}
@Override
protected Source openSource() throws IOException{
if(tempFile==null){
return Okio.source(MastodonApp.context.getContentResolver().openInputStream(uri));
}else{
return Okio.source(tempFile);
}
}
@Override
public MediaType contentType(){
return MediaType.get(contentType);
}
@Override
public void writeTo(BufferedSink sink) throws IOException{
try{
super.writeTo(sink);
}finally{
if(tempFile!=null){
tempFile.delete();
}
}
}
}

View File

@ -8,31 +8,40 @@ import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.ContentUriRequestBody; import org.joinmastodon.android.api.ContentUriRequestBody;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.ProgressListener; import org.joinmastodon.android.api.ProgressListener;
import org.joinmastodon.android.api.ResizedImageRequestBody;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.IOException;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody; import okhttp3.RequestBody;
public class UploadAttachment extends MastodonAPIRequest<Attachment>{ public class UploadAttachment extends MastodonAPIRequest<Attachment>{
private Uri uri; private Uri uri;
private ProgressListener progressListener; private ProgressListener progressListener;
private int maxImageSize;
public UploadAttachment(Uri uri){ public UploadAttachment(Uri uri){
super(HttpMethod.POST, "/media", Attachment.class); super(HttpMethod.POST, "/media", Attachment.class);
this.uri=uri; this.uri=uri;
} }
public UploadAttachment(Uri uri, int maxImageSize){
this(uri);
this.maxImageSize=maxImageSize;
}
public UploadAttachment setProgressListener(ProgressListener progressListener){ public UploadAttachment setProgressListener(ProgressListener progressListener){
this.progressListener=progressListener; this.progressListener=progressListener;
return this; return this;
} }
@Override @Override
public RequestBody getRequestBody(){ public RequestBody getRequestBody() throws IOException{
return new MultipartBody.Builder() return new MultipartBody.Builder()
.setType(MultipartBody.FORM) .setType(MultipartBody.FORM)
.addFormDataPart("file", UiUtils.getFileName(uri), new ContentUriRequestBody(uri, progressListener)) .addFormDataPart("file", UiUtils.getFileName(uri), maxImageSize>0 ? new ResizedImageRequestBody(uri, maxImageSize, progressListener) : new ContentUriRequestBody(uri, progressListener))
.build(); .build();
} }
} }

View File

@ -12,25 +12,20 @@ public class AccountSession{
public Token token; public Token token;
public Account self; public Account self;
public String domain; public String domain;
public int tootCharLimit;
public Application app; public Application app;
public long infoLastUpdated; public long infoLastUpdated;
public long instanceLastUpdated;
public Instance instance;
public boolean activated=true; public boolean activated=true;
private transient MastodonAPIController apiController; private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController; private transient StatusInteractionController statusInteractionController;
private transient CacheController cacheController; private transient CacheController cacheController;
AccountSession(Token token, Account self, Application app, String domain, int tootCharLimit, Instance instance, boolean activated){ AccountSession(Token token, Account self, Application app, String domain, boolean activated){
this.token=token; this.token=token;
this.self=self; this.self=self;
this.domain=domain; this.domain=domain;
this.app=app; this.app=app;
this.tootCharLimit=tootCharLimit;
this.instance=instance;
this.activated=activated; this.activated=activated;
instanceLastUpdated=infoLastUpdated=System.currentTimeMillis(); infoLastUpdated=System.currentTimeMillis();
} }
AccountSession(){} AccountSession(){}

View File

@ -1,7 +1,6 @@
package org.joinmastodon.android.api.session; package org.joinmastodon.android.api.session;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
@ -37,12 +36,10 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsIntent;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
@ -56,13 +53,14 @@ public class AccountSessionManager{
private HashMap<String, AccountSession> sessions=new HashMap<>(); private HashMap<String, AccountSession> sessions=new HashMap<>();
private HashMap<String, List<EmojiCategory>> customEmojis=new HashMap<>(); private HashMap<String, List<EmojiCategory>> customEmojis=new HashMap<>();
private HashMap<String, Long> customEmojisLastUpdated=new HashMap<>(); private HashMap<String, Long> instancesLastUpdated=new HashMap<>();
private HashMap<String, Instance> instances=new HashMap<>();
private MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null); private MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null);
private Instance authenticatingInstance; private Instance authenticatingInstance;
private Application authenticatingApp; private Application authenticatingApp;
private String lastActiveAccountID; private String lastActiveAccountID;
private SharedPreferences prefs; private SharedPreferences prefs;
private boolean loadedCustomEmojis; private boolean loadedInstances;
public static AccountSessionManager getInstance(){ public static AccountSessionManager getInstance(){
return instance; return instance;
@ -84,14 +82,16 @@ public class AccountSessionManager{
Log.e(TAG, "Error loading accounts", x); Log.e(TAG, "Error loading accounts", x);
} }
lastActiveAccountID=prefs.getString("lastActiveAccount", null); lastActiveAccountID=prefs.getString("lastActiveAccount", null);
MastodonAPIController.runInBackground(()->readCustomEmojis(domains)); MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
} }
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){ public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
AccountSession session=new AccountSession(token, self, app, instance.uri, instance.maxTootChars, instance, active); instances.put(instance.uri, instance);
AccountSession session=new AccountSession(token, self, app, instance.uri, active);
sessions.put(session.getID(), session); sessions.put(session.getID(), session);
lastActiveAccountID=session.getID(); lastActiveAccountID=session.getID();
writeAccountsFile(); writeAccountsFile();
maybeUpdateLocalInfo();
} }
private void writeAccountsFile(){ private void writeAccountsFile(){
@ -155,7 +155,7 @@ public class AccountSessionManager{
writeAccountsFile(); writeAccountsFile();
String domain=session.domain.toLowerCase(); String domain=session.domain.toLowerCase();
if(sessions.isEmpty() || !sessions.values().stream().map(s->s.domain.toLowerCase()).collect(Collectors.toSet()).contains(domain)){ if(sessions.isEmpty() || !sessions.values().stream().map(s->s.domain.toLowerCase()).collect(Collectors.toSet()).contains(domain)){
getCustomEmojisFile(domain).delete(); getInstanceInfoFile(domain).delete();
} }
} }
@ -213,11 +213,11 @@ public class AccountSessionManager{
HashSet<String> domains=new HashSet<>(); HashSet<String> domains=new HashSet<>();
for(AccountSession session:sessions.values()){ for(AccountSession session:sessions.values()){
domains.add(session.domain.toLowerCase()); domains.add(session.domain.toLowerCase());
if(now-session.infoLastUpdated>24L*3600_000L || now-session.instanceLastUpdated>24L*360_000L*3L){ if(now-session.infoLastUpdated>24L*3600_000L){
updateSessionLocalInfo(session); updateSessionLocalInfo(session);
} }
} }
if(loadedCustomEmojis){ if(loadedInstances){
maybeUpdateCustomEmojis(domains); maybeUpdateCustomEmojis(domains);
} }
} }
@ -225,9 +225,9 @@ public class AccountSessionManager{
private void maybeUpdateCustomEmojis(Set<String> domains){ private void maybeUpdateCustomEmojis(Set<String> domains){
long now=System.currentTimeMillis(); long now=System.currentTimeMillis();
for(String domain:domains){ for(String domain:domains){
Long lastUpdated=customEmojisLastUpdated.get(domain); Long lastUpdated=instancesLastUpdated.get(domain);
if(lastUpdated==null || now-lastUpdated>24L*3600_000L){ if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
updateCustomEmojis(domain); updateInstanceInfo(domain);
} }
} }
} }
@ -248,34 +248,33 @@ public class AccountSessionManager{
} }
}) })
.exec(session.getID()); .exec(session.getID());
}
private void updateInstanceInfo(String domain){
new GetInstance() new GetInstance()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Instance result){ public void onSuccess(Instance instance){
session.instance=result; instances.put(domain, instance);
session.instanceLastUpdated=System.currentTimeMillis(); new GetCustomEmojis()
writeAccountsFile(); .setCallback(new Callback<>(){
} @Override
public void onSuccess(List<Emoji> result){
InstanceInfoStorageWrapper emojis=new InstanceInfoStorageWrapper();
emojis.lastUpdated=System.currentTimeMillis();
emojis.emojis=result;
emojis.instance=instance;
customEmojis.put(domain, groupCustomEmojis(emojis));
instancesLastUpdated.put(domain, emojis.lastUpdated);
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(emojis, domain));
}
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
} }
}) })
.exec(session.getID()); .execNoAuth(domain);
}
private void updateCustomEmojis(String domain){
new GetCustomEmojis()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Emoji> result){
CustomEmojisStorageWrapper emojis=new CustomEmojisStorageWrapper();
emojis.lastUpdated=System.currentTimeMillis();
emojis.emojis=result;
customEmojis.put(domain, groupCustomEmojis(emojis));
customEmojisLastUpdated.put(domain, emojis.lastUpdated);
MastodonAPIController.runInBackground(()->writeCustomEmojisFile(emojis, domain));
} }
@Override @Override
@ -286,38 +285,39 @@ public class AccountSessionManager{
.execNoAuth(domain); .execNoAuth(domain);
} }
private File getCustomEmojisFile(String domain){ private File getInstanceInfoFile(String domain){
return new File(MastodonApp.context.getFilesDir(), "emojis_"+domain.replace('.', '_')+".json"); return new File(MastodonApp.context.getFilesDir(), "instance_"+domain.replace('.', '_')+".json");
} }
private void writeCustomEmojisFile(CustomEmojisStorageWrapper emojis, String domain){ private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
try(FileOutputStream out=new FileOutputStream(getCustomEmojisFile(domain))){ try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8); OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(emojis, writer); MastodonAPIController.gson.toJson(emojis, writer);
writer.flush(); writer.flush();
}catch(IOException x){ }catch(IOException x){
Log.w(TAG, "Error writing emojis file for "+domain, x); Log.w(TAG, "Error writing instance info file for "+domain, x);
} }
} }
private void readCustomEmojis(Set<String> domains){ private void readInstanceInfo(Set<String> domains){
for(String domain:domains){ for(String domain:domains){
try(FileInputStream in=new FileInputStream(getCustomEmojisFile(domain))){ try(FileInputStream in=new FileInputStream(getInstanceInfoFile(domain))){
InputStreamReader reader=new InputStreamReader(in, StandardCharsets.UTF_8); InputStreamReader reader=new InputStreamReader(in, StandardCharsets.UTF_8);
CustomEmojisStorageWrapper emojis=MastodonAPIController.gson.fromJson(reader, CustomEmojisStorageWrapper.class); InstanceInfoStorageWrapper emojis=MastodonAPIController.gson.fromJson(reader, InstanceInfoStorageWrapper.class);
customEmojis.put(domain, groupCustomEmojis(emojis)); customEmojis.put(domain, groupCustomEmojis(emojis));
customEmojisLastUpdated.put(domain, emojis.lastUpdated); instances.put(domain, emojis.instance);
instancesLastUpdated.put(domain, emojis.lastUpdated);
}catch(IOException|JsonParseException x){ }catch(IOException|JsonParseException x){
Log.w(TAG, "Error reading emojis file for "+domain, x); Log.w(TAG, "Error reading instance info file for "+domain, x);
} }
} }
if(!loadedCustomEmojis){ if(!loadedInstances){
loadedCustomEmojis=true; loadedInstances=true;
maybeUpdateCustomEmojis(domains); maybeUpdateCustomEmojis(domains);
} }
} }
private List<EmojiCategory> groupCustomEmojis(CustomEmojisStorageWrapper emojis){ private List<EmojiCategory> groupCustomEmojis(InstanceInfoStorageWrapper emojis){
return emojis.emojis.stream() return emojis.emojis.stream()
.filter(e->e.visibleInPicker) .filter(e->e.visibleInPicker)
.collect(Collectors.groupingBy(e->e.category==null ? "" : e.category)) .collect(Collectors.groupingBy(e->e.category==null ? "" : e.category))
@ -333,6 +333,10 @@ public class AccountSessionManager{
return r==null ? Collections.emptyList() : r; return r==null ? Collections.emptyList() : r;
} }
public Instance getInstanceInfo(String domain){
return instances.get(domain);
}
public void updateAccountInfo(String id, Account account){ public void updateAccountInfo(String id, Account account){
AccountSession session=getAccount(id); AccountSession session=getAccount(id);
session.self=account; session.self=account;
@ -344,7 +348,8 @@ public class AccountSessionManager{
public List<AccountSession> accounts; public List<AccountSession> accounts;
} }
private static class CustomEmojisStorageWrapper{ private static class InstanceInfoStorageWrapper{
public Instance instance;
public List<Emoji> emojis; public List<Emoji> emojis;
public long lastUpdated; public long lastUpdated;
} }

View File

@ -18,6 +18,7 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.text.Editable; import android.text.Editable;
import android.text.InputFilter;
import android.text.Layout; import android.text.Layout;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
@ -62,6 +63,7 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Mention; import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.model.StatusPrivacy;
@ -104,7 +106,6 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
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_POLL_OPTIONS=4;
private static final int MAX_ATTACHMENTS=4; private static final int MAX_ATTACHMENTS=4;
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);
@ -173,6 +174,7 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
private ComposeAutocompleteSpan currentAutocompleteSpan; private ComposeAutocompleteSpan currentAutocompleteSpan;
private FrameLayout mainEditTextWrap; private FrameLayout mainEditTextWrap;
private ComposeAutocompleteViewController autocompleteViewController; private ComposeAutocompleteViewController autocompleteViewController;
private Instance instance;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
@ -181,12 +183,22 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID); AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
charLimit=session.tootCharLimit;
if(charLimit==0)
charLimit=500;
self=session.self; self=session.self;
instanceDomain=session.domain; instanceDomain=session.domain;
customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain); customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain);
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
if(instance==null){
Nav.finish(this);
return;
}
if(instance.maxTootChars>0)
charLimit=instance.maxTootChars;
else if(instance.configuration!=null && instance.configuration.statuses!=null && instance.configuration.statuses.maxCharacters>0)
charLimit=instance.configuration.statuses.maxCharacters;
else
charLimit=500;
if(getArguments().containsKey("replyTo")){ if(getArguments().containsKey("replyTo")){
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo")); replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
statusVisibility=replyTo.visibility; statusVisibility=replyTo.visibility;
@ -647,7 +659,11 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
Intent intent=new Intent(Intent.ACTION_GET_CONTENT); Intent intent=new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE); intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*"); intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"}); if(instance.configuration!=null && instance.configuration.mediaAttachments!=null && instance.configuration.mediaAttachments.supportedMimeTypes!=null && !instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()){
intent.putExtra(Intent.EXTRA_MIME_TYPES, instance.configuration.mediaAttachments.supportedMimeTypes.toArray(new String[0]));
}else{
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
}
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
startActivityForResult(intent, MEDIA_RESULT); startActivityForResult(intent, MEDIA_RESULT);
} }
@ -740,7 +756,12 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
rotationAnimator.setDuration(1500); rotationAnimator.setDuration(1500);
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE); rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
rotationAnimator.start(); rotationAnimator.start();
attachment.uploadRequest=(UploadAttachment) new UploadAttachment(attachment.uri) int maxSize=0;
String contentType=getActivity().getContentResolver().getType(attachment.uri);
if(contentType!=null && contentType.startsWith("image/")){
maxSize=2_073_600; // TODO get this from instance configuration when it gets added there
}
attachment.uploadRequest=(UploadAttachment) new UploadAttachment(attachment.uri, maxSize)
.setProgressListener(new ProgressListener(){ .setProgressListener(new ProgressListener(){
@Override @Override
public void onProgress(long transferred, long total){ public void onProgress(long transferred, long total){
@ -864,10 +885,11 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
return true; return true;
}); });
option.edit.addTextChangedListener(new SimpleTextWatcher(e->updatePublishButtonState())); option.edit.addTextChangedListener(new SimpleTextWatcher(e->updatePublishButtonState()));
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)});
pollOptionsView.addView(option.view); pollOptionsView.addView(option.view);
pollOptions.add(option); pollOptions.add(option);
if(pollOptions.size()==MAX_POLL_OPTIONS) if(pollOptions.size()==(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0 ? instance.configuration.polls.maxOptions : 4))
addPollOptionBtn.setVisibility(View.GONE); addPollOptionBtn.setVisibility(View.GONE);
return option; return option;
} }

View File

@ -132,7 +132,7 @@ public class AccountActivationFragment extends AppKitFragment{
AccountSessionManager mgr=AccountSessionManager.getInstance(); AccountSessionManager mgr=AccountSessionManager.getInstance();
AccountSession session=mgr.getAccount(accountID); AccountSession session=mgr.getAccount(accountID);
mgr.removeAccount(accountID); mgr.removeAccount(accountID);
mgr.addAccount(session.instance, session.token, result, session.app, true); mgr.addAccount(mgr.getInstanceInfo(session.domain), session.token, result, session.app, true);
String newID=mgr.getLastActiveAccountID(); String newID=mgr.getLastActiveAccountID();
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", newID); args.putString("account", newID);

View File

@ -21,7 +21,7 @@ public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
@Override @Override
protected void populateItems(){ protected void populateItems(){
isMultipleChoice=true; isMultipleChoice=true;
Instance inst=AccountSessionManager.getInstance().getAccount(accountID).instance; Instance inst=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.getInstance().getAccount(accountID).domain);
if(inst!=null && inst.rules!=null){ if(inst!=null && inst.rules!=null){
for(Instance.Rule rule:inst.rules){ for(Instance.Rule rule:inst.rules){
items.add(new Item(rule.text, null, rule.id)); items.add(new Item(rule.text, null, rule.id));

View File

@ -76,8 +76,11 @@ public class Instance extends BaseModel{
public Account contactAccount; public Account contactAccount;
public Stats stats; public Stats stats;
public int maxTootChars;
public List<Rule> rules; public List<Rule> rules;
public Configuration configuration;
// non-standard field in some Mastodon forks
public int maxTootChars;
@Override @Override
public void postprocess() throws ObjectValidationException{ public void postprocess() throws ObjectValidationException{
@ -137,4 +140,36 @@ public class Instance extends BaseModel{
public int statusCount; public int statusCount;
public int domainCount; public int domainCount;
} }
@Parcel
public static class Configuration{
public StatusesConfiguration statuses;
public MediaAttachmentsConfiguration mediaAttachments;
public PollsConfiguration polls;
}
@Parcel
public static class StatusesConfiguration{
public int maxCharacters;
public int maxMediaAttachments;
public int charactersReservedPerUrl;
}
@Parcel
public static class MediaAttachmentsConfiguration{
public List<String> supportedMimeTypes;
public int imageSizeLimit;
public int imageMatrixLimit;
public int videoSizeLimit;
public int videoFrameRateLimit;
public int videoMatrixLimit;
}
@Parcel
public static class PollsConfiguration{
public int maxOptions;
public int maxCharactersPerOption;
public int minExpiration;
public int maxExpiration;
}
} }