diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 6fede924..32866fc5 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -10,7 +10,7 @@ android { applicationId "org.joinmastodon.android" minSdk 23 targetSdk 31 - versionCode 15 + versionCode 16 versionName "0.1" } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/ContentUriRequestBody.java b/mastodon/src/main/java/org/joinmastodon/android/api/ContentUriRequestBody.java index 507ad861..fa7e76f2 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/ContentUriRequestBody.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/ContentUriRequestBody.java @@ -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)); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/CountingRequestBody.java b/mastodon/src/main/java/org/joinmastodon/android/api/CountingRequestBody.java new file mode 100644 index 00000000..4af1a083 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/CountingRequestBody.java @@ -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; +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/CountingSink.java b/mastodon/src/main/java/org/joinmastodon/android/api/CountingSink.java new file mode 100644 index 00000000..f53838e1 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/CountingSink.java @@ -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)); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java index 55af2088..a72e5840 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java @@ -135,7 +135,7 @@ public abstract class MastodonAPIRequest extends APIRequest{ return method; } - public RequestBody getRequestBody(){ + public RequestBody getRequestBody() throws IOException{ return requestBody==null ? null : new JsonObjectRequestBody(requestBody); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/ResizedImageRequestBody.java b/mastodon/src/main/java/org/joinmastodon/android/api/ResizedImageRequestBody.java new file mode 100644 index 00000000..cbc0c387 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/ResizedImageRequestBody.java @@ -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(); + } + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/UploadAttachment.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/UploadAttachment.java index af96bf0d..22e0c562 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/UploadAttachment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/UploadAttachment.java @@ -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{ 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(); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java index 6d8b64f8..427bf411 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java @@ -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(){} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index 0263f7b2..014c8741 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -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 sessions=new HashMap<>(); private HashMap> customEmojis=new HashMap<>(); - private HashMap customEmojisLastUpdated=new HashMap<>(); + private HashMap instancesLastUpdated=new HashMap<>(); + private HashMap 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 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 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,33 @@ 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(); - } + public void onSuccess(Instance instance){ + instances.put(domain, instance); + new GetCustomEmojis() + .setCallback(new Callback<>(){ + @Override + public void onSuccess(List 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 - public void onError(ErrorResponse error){ + @Override + public void onError(ErrorResponse error){ - } - }) - .exec(session.getID()); - } - - private void updateCustomEmojis(String domain){ - new GetCustomEmojis() - .setCallback(new Callback<>(){ - @Override - public void onSuccess(List 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)); + } + }) + .execNoAuth(domain); } @Override @@ -286,38 +285,39 @@ public class AccountSessionManager{ .execNoAuth(domain); } - private File getCustomEmojisFile(String domain){ - return new File(MastodonApp.context.getFilesDir(), "emojis_"+domain.replace('.', '_')+".json"); + private File getInstanceInfoFile(String domain){ + return new File(MastodonApp.context.getFilesDir(), "instance_"+domain.replace('.', '_')+".json"); } - private void writeCustomEmojisFile(CustomEmojisStorageWrapper emojis, String domain){ - try(FileOutputStream out=new FileOutputStream(getCustomEmojisFile(domain))){ + 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 domains){ + private void readInstanceInfo(Set 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 groupCustomEmojis(CustomEmojisStorageWrapper emojis){ + private List 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 accounts; } - private static class CustomEmojisStorageWrapper{ + private static class InstanceInfoStorageWrapper{ + public Instance instance; public List emojis; public long lastUpdated; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index 4f59fecd..ecfc985a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -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("*/*"); - 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); 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; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java index d3e04219..c8a5946c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java @@ -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); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportRuleChoiceFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportRuleChoiceFragment.java index cd61bf81..7d4f5a0e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportRuleChoiceFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportRuleChoiceFragment.java @@ -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)); diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java index 05009877..587ce94b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java @@ -76,8 +76,11 @@ public class Instance extends BaseModel{ public Account contactAccount; public Stats stats; - public int maxTootChars; public List 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 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; + } }