Image resizing
This commit is contained in:
parent
d47eb752a5
commit
e087cf03cc
|
@ -10,7 +10,7 @@ android {
|
|||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 31
|
||||
versionCode 15
|
||||
versionCode 16
|
||||
versionName "0.1"
|
||||
}
|
||||
|
||||
|
|
|
@ -2,31 +2,22 @@ package org.joinmastodon.android.api;
|
|||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.provider.OpenableColumns;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSink;
|
||||
import okio.ForwardingSink;
|
||||
import okio.Okio;
|
||||
import okio.Sink;
|
||||
import okio.Source;
|
||||
|
||||
public class ContentUriRequestBody extends RequestBody{
|
||||
public class ContentUriRequestBody extends CountingRequestBody{
|
||||
private final Uri uri;
|
||||
private final long length;
|
||||
private ProgressListener progressListener;
|
||||
|
||||
public ContentUriRequestBody(Uri uri, ProgressListener progressListener){
|
||||
super(progressListener);
|
||||
this.uri=uri;
|
||||
this.progressListener=progressListener;
|
||||
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
|
||||
cursor.moveToFirst();
|
||||
length=cursor.getInt(0);
|
||||
|
@ -39,40 +30,7 @@ public class ContentUriRequestBody extends RequestBody{
|
|||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException{
|
||||
return length;
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
protected Source openSource() throws IOException{
|
||||
return Okio.source(MastodonApp.context.getContentResolver().openInputStream(uri));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -135,7 +135,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||
return method;
|
||||
}
|
||||
|
||||
public RequestBody getRequestBody(){
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,31 +8,40 @@ import org.joinmastodon.android.MastodonApp;
|
|||
import org.joinmastodon.android.api.ContentUriRequestBody;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.ProgressListener;
|
||||
import org.joinmastodon.android.api.ResizedImageRequestBody;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
public class UploadAttachment extends MastodonAPIRequest<Attachment>{
|
||||
private Uri uri;
|
||||
private ProgressListener progressListener;
|
||||
private int maxImageSize;
|
||||
|
||||
public UploadAttachment(Uri uri){
|
||||
super(HttpMethod.POST, "/media", Attachment.class);
|
||||
this.uri=uri;
|
||||
}
|
||||
|
||||
public UploadAttachment(Uri uri, int maxImageSize){
|
||||
this(uri);
|
||||
this.maxImageSize=maxImageSize;
|
||||
}
|
||||
|
||||
public UploadAttachment setProgressListener(ProgressListener progressListener){
|
||||
this.progressListener=progressListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody getRequestBody(){
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
return new MultipartBody.Builder()
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,25 +12,20 @@ public class AccountSession{
|
|||
public Token token;
|
||||
public Account self;
|
||||
public String domain;
|
||||
public int tootCharLimit;
|
||||
public Application app;
|
||||
public long infoLastUpdated;
|
||||
public long instanceLastUpdated;
|
||||
public Instance instance;
|
||||
public boolean activated=true;
|
||||
private transient MastodonAPIController apiController;
|
||||
private transient StatusInteractionController statusInteractionController;
|
||||
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.self=self;
|
||||
this.domain=domain;
|
||||
this.app=app;
|
||||
this.tootCharLimit=tootCharLimit;
|
||||
this.instance=instance;
|
||||
this.activated=activated;
|
||||
instanceLastUpdated=infoLastUpdated=System.currentTimeMillis();
|
||||
infoLastUpdated=System.currentTimeMillis();
|
||||
}
|
||||
|
||||
AccountSession(){}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
|
@ -37,12 +36,10 @@ import java.util.HashMap;
|
|||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
@ -56,13 +53,14 @@ public class AccountSessionManager{
|
|||
|
||||
private HashMap<String, AccountSession> sessions=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 Instance authenticatingInstance;
|
||||
private Application authenticatingApp;
|
||||
private String lastActiveAccountID;
|
||||
private SharedPreferences prefs;
|
||||
private boolean loadedCustomEmojis;
|
||||
private boolean loadedInstances;
|
||||
|
||||
public static AccountSessionManager getInstance(){
|
||||
return instance;
|
||||
|
@ -84,14 +82,16 @@ public class AccountSessionManager{
|
|||
Log.e(TAG, "Error loading accounts", x);
|
||||
}
|
||||
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){
|
||||
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);
|
||||
lastActiveAccountID=session.getID();
|
||||
writeAccountsFile();
|
||||
maybeUpdateLocalInfo();
|
||||
}
|
||||
|
||||
private void writeAccountsFile(){
|
||||
|
@ -155,7 +155,7 @@ public class AccountSessionManager{
|
|||
writeAccountsFile();
|
||||
String domain=session.domain.toLowerCase();
|
||||
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<>();
|
||||
for(AccountSession session:sessions.values()){
|
||||
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);
|
||||
}
|
||||
}
|
||||
if(loadedCustomEmojis){
|
||||
if(loadedInstances){
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
}
|
||||
}
|
||||
|
@ -225,9 +225,9 @@ public class AccountSessionManager{
|
|||
private void maybeUpdateCustomEmojis(Set<String> domains){
|
||||
long now=System.currentTimeMillis();
|
||||
for(String domain:domains){
|
||||
Long lastUpdated=customEmojisLastUpdated.get(domain);
|
||||
Long lastUpdated=instancesLastUpdated.get(domain);
|
||||
if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
|
||||
updateCustomEmojis(domain);
|
||||
updateInstanceInfo(domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -248,34 +248,25 @@ public class AccountSessionManager{
|
|||
}
|
||||
})
|
||||
.exec(session.getID());
|
||||
}
|
||||
|
||||
private void updateInstanceInfo(String domain){
|
||||
new GetInstance()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Instance result){
|
||||
session.instance=result;
|
||||
session.instanceLastUpdated=System.currentTimeMillis();
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
}
|
||||
})
|
||||
.exec(session.getID());
|
||||
}
|
||||
|
||||
private void updateCustomEmojis(String domain){
|
||||
public void onSuccess(Instance instance){
|
||||
instances.put(domain, instance);
|
||||
new GetCustomEmojis()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Emoji> result){
|
||||
CustomEmojisStorageWrapper emojis=new CustomEmojisStorageWrapper();
|
||||
InstanceInfoStorageWrapper emojis=new InstanceInfoStorageWrapper();
|
||||
emojis.lastUpdated=System.currentTimeMillis();
|
||||
emojis.emojis=result;
|
||||
emojis.instance=instance;
|
||||
customEmojis.put(domain, groupCustomEmojis(emojis));
|
||||
customEmojisLastUpdated.put(domain, emojis.lastUpdated);
|
||||
MastodonAPIController.runInBackground(()->writeCustomEmojisFile(emojis, domain));
|
||||
instancesLastUpdated.put(domain, emojis.lastUpdated);
|
||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(emojis, domain));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -286,38 +277,47 @@ public class AccountSessionManager{
|
|||
.execNoAuth(domain);
|
||||
}
|
||||
|
||||
private File getCustomEmojisFile(String domain){
|
||||
return new File(MastodonApp.context.getFilesDir(), "emojis_"+domain.replace('.', '_')+".json");
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
}
|
||||
})
|
||||
.execNoAuth(domain);
|
||||
}
|
||||
|
||||
private void writeCustomEmojisFile(CustomEmojisStorageWrapper emojis, String domain){
|
||||
try(FileOutputStream out=new FileOutputStream(getCustomEmojisFile(domain))){
|
||||
private File getInstanceInfoFile(String domain){
|
||||
return new File(MastodonApp.context.getFilesDir(), "instance_"+domain.replace('.', '_')+".json");
|
||||
}
|
||||
|
||||
private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
|
||||
try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){
|
||||
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(emojis, writer);
|
||||
writer.flush();
|
||||
}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){
|
||||
try(FileInputStream in=new FileInputStream(getCustomEmojisFile(domain))){
|
||||
try(FileInputStream in=new FileInputStream(getInstanceInfoFile(domain))){
|
||||
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));
|
||||
customEmojisLastUpdated.put(domain, emojis.lastUpdated);
|
||||
instances.put(domain, emojis.instance);
|
||||
instancesLastUpdated.put(domain, emojis.lastUpdated);
|
||||
}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){
|
||||
loadedCustomEmojis=true;
|
||||
if(!loadedInstances){
|
||||
loadedInstances=true;
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
}
|
||||
}
|
||||
|
||||
private List<EmojiCategory> groupCustomEmojis(CustomEmojisStorageWrapper emojis){
|
||||
private List<EmojiCategory> groupCustomEmojis(InstanceInfoStorageWrapper emojis){
|
||||
return emojis.emojis.stream()
|
||||
.filter(e->e.visibleInPicker)
|
||||
.collect(Collectors.groupingBy(e->e.category==null ? "" : e.category))
|
||||
|
@ -333,6 +333,10 @@ public class AccountSessionManager{
|
|||
return r==null ? Collections.emptyList() : r;
|
||||
}
|
||||
|
||||
public Instance getInstanceInfo(String domain){
|
||||
return instances.get(domain);
|
||||
}
|
||||
|
||||
public void updateAccountInfo(String id, Account account){
|
||||
AccountSession session=getAccount(id);
|
||||
session.self=account;
|
||||
|
@ -344,7 +348,8 @@ public class AccountSessionManager{
|
|||
public List<AccountSession> accounts;
|
||||
}
|
||||
|
||||
private static class CustomEmojisStorageWrapper{
|
||||
private static class InstanceInfoStorageWrapper{
|
||||
public Instance instance;
|
||||
public List<Emoji> emojis;
|
||||
public long lastUpdated;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import android.os.Build;
|
|||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.Layout;
|
||||
import android.text.SpannableStringBuilder;
|
||||
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.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
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 IMAGE_DESCRIPTION_RESULT=363;
|
||||
private static final int MAX_POLL_OPTIONS=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);
|
||||
|
@ -173,6 +174,7 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
|
|||
private ComposeAutocompleteSpan currentAutocompleteSpan;
|
||||
private FrameLayout mainEditTextWrap;
|
||||
private ComposeAutocompleteViewController autocompleteViewController;
|
||||
private Instance instance;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
|
@ -181,12 +183,22 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
|
|||
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
charLimit=session.tootCharLimit;
|
||||
if(charLimit==0)
|
||||
charLimit=500;
|
||||
self=session.self;
|
||||
instanceDomain=session.domain;
|
||||
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")){
|
||||
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
|
||||
statusVisibility=replyTo.visibility;
|
||||
|
@ -647,7 +659,11 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
|
|||
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
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);
|
||||
startActivityForResult(intent, MEDIA_RESULT);
|
||||
}
|
||||
|
@ -740,7 +756,12 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
|
|||
rotationAnimator.setDuration(1500);
|
||||
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
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(){
|
||||
@Override
|
||||
public void onProgress(long transferred, long total){
|
||||
|
@ -864,10 +885,11 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
|
|||
return true;
|
||||
});
|
||||
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);
|
||||
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);
|
||||
return option;
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ public class AccountActivationFragment extends AppKitFragment{
|
|||
AccountSessionManager mgr=AccountSessionManager.getInstance();
|
||||
AccountSession session=mgr.getAccount(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();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", newID);
|
||||
|
|
|
@ -21,7 +21,7 @@ public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
|
|||
@Override
|
||||
protected void populateItems(){
|
||||
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){
|
||||
for(Instance.Rule rule:inst.rules){
|
||||
items.add(new Item(rule.text, null, rule.id));
|
||||
|
|
|
@ -76,8 +76,11 @@ public class Instance extends BaseModel{
|
|||
public Account contactAccount;
|
||||
public Stats stats;
|
||||
|
||||
public int maxTootChars;
|
||||
public List<Rule> rules;
|
||||
public Configuration configuration;
|
||||
|
||||
// non-standard field in some Mastodon forks
|
||||
public int maxTootChars;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
|
@ -137,4 +140,36 @@ public class Instance extends BaseModel{
|
|||
public int statusCount;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue