added status edit media support, bug fix

This commit is contained in:
nuclearfog 2023-04-21 16:45:42 +02:00
parent 0baaed7579
commit 7aa702604a
No known key found for this signature in database
GPG Key ID: 03488A185C476379
14 changed files with 216 additions and 130 deletions

View File

@ -420,7 +420,7 @@ public interface Connection {
* @param mediaIds IDs of the uploaded media files if any
* @return uploaded status
*/
Status uploadStatus(StatusUpdate update, long[] mediaIds) throws ConnectionException;
Status uploadStatus(StatusUpdate update, List<Long> mediaIds) throws ConnectionException;
/**
* create userlist

View File

@ -598,7 +598,7 @@ public class Mastodon implements Connection {
@Override
public Status uploadStatus(StatusUpdate update, long[] mediaIds) throws MastodonException {
public Status uploadStatus(StatusUpdate update, List<Long> mediaIds) throws MastodonException {
List<String> params = new ArrayList<>();
// add identifier to prevent duplicate posts
params.add("Idempotency-Key=" + System.currentTimeMillis() / 5000);
@ -619,8 +619,13 @@ public class Mastodon implements Connection {
params.add("visibility=unlisted");
else
params.add("visibility=public");
for (long mediaId : mediaIds)
for (long mediaId : mediaIds) {
params.add("media_ids[]=" + mediaId);
}
// add media keys of a previous status
for (String mediaKey : update.getMediaKeys()) {
params.add("media_ids[]=" + mediaKey);
}
if (update.getPoll() != null) {
PollUpdate poll = update.getPoll();
for (String option : poll.getOptions())

View File

@ -722,7 +722,7 @@ public class TwitterV1 implements Connection {
@Override
public Status uploadStatus(StatusUpdate update, long[] mediaIds) throws TwitterException {
public Status uploadStatus(StatusUpdate update, List<Long> mediaIds) throws TwitterException {
List<String> params = new ArrayList<>();
if (update.getText() != null)
params.add("status=" + StringUtils.encode(update.getText()));
@ -730,7 +730,7 @@ public class TwitterV1 implements Connection {
params.add("in_reply_to_status_id=" + update.getReplyId());
if (update.isSensitive())
params.add("possibly_sensitive=true");
if (mediaIds != null && mediaIds.length > 0) {
if (!mediaIds.isEmpty()) {
StringBuilder buf = new StringBuilder();
for (long id : mediaIds)
buf.append(id).append("%2C");

View File

@ -13,6 +13,9 @@ import org.nuclearfog.twidda.backend.helper.StatusUpdate;
import org.nuclearfog.twidda.model.Status;
import org.nuclearfog.twidda.ui.activities.StatusEditor;
import java.util.LinkedList;
import java.util.List;
/**
* Background task for posting a status
*
@ -35,11 +38,12 @@ public class StatusUpdater extends AsyncExecutor<StatusUpdate, StatusUpdater.Sta
protected StatusUpdateResult doInBackground(@NonNull StatusUpdate update) {
try {
// upload media first
MediaStatus[] mediaUpdates = update.getMediaUpdates();
long[] mediaIds = new long[mediaUpdates.length];
for (int pos = 0; pos < mediaUpdates.length; pos++) {
// upload media file and save media ID
mediaIds[pos] = connection.uploadMedia(mediaUpdates[pos]);
List<Long> mediaIds = new LinkedList<>();
for (MediaStatus mediaStatus : update.getMediaStatuses()) {
if (mediaStatus.isLocal()) {
long mediaId = connection.uploadMedia(mediaStatus);
mediaIds.add(mediaId);
}
}
// upload status
Status status = connection.uploadStatus(update, mediaIds);

View File

@ -1,5 +1,8 @@
package org.nuclearfog.twidda.backend.helper;
import android.content.ContentResolver;
import android.net.Uri;
import androidx.annotation.NonNull;
import java.io.IOException;
@ -18,13 +21,52 @@ public class MediaStatus implements Serializable {
private InputStream inputStream;
private String mimeType;
private String path;
private boolean local;
/**
* @param inputStream stream of the media (local or online)
* @param mimeType MIME type e.g. image/jpeg
* create MediaStatus from an online source
*
* @param inputStream inputstream to fetch data from internet
* @param mimeType MIME type of the media
*/
public MediaStatus(InputStream inputStream, String mimeType) {
this.inputStream = inputStream;
this.mimeType = mimeType;
local = false;
}
/**
* create MediaStatus from an offline source
*
* @param path path to the local file
* @param mimeType MIME type of the file
*/
public MediaStatus(String path, String mimeType) {
this.path = path;
this.mimeType = mimeType;
local = true;
}
/**
* create a stream to upload media file
*
* @param resolver content resolver used to create stream and determine MIME type of the file
* @return true if stream is prepared, false if an error occured
*/
public boolean openStream(ContentResolver resolver) {
if (path == null)
return false;
Uri uri = Uri.parse(path);
try {
inputStream = resolver.openInputStream(uri);
mimeType = resolver.getType(uri);
// check if stream is valid
return inputStream != null && mimeType != null && inputStream.available() > 0;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
/**
@ -52,6 +94,10 @@ public class MediaStatus implements Serializable {
}
}
public boolean isLocal() {
return local;
}
/**
* close stream
*/

View File

@ -10,8 +10,6 @@ import androidx.documentfile.provider.DocumentFile;
import org.nuclearfog.twidda.model.Instance;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Set;
@ -26,7 +24,7 @@ public class MessageUpdate implements Serializable {
private static final long serialVersionUID = 991295406939128220L;
private String uriString;
private String mediaUri;
private MediaStatus mediaUpdate;
private String name = "";
private String text = "";
@ -84,7 +82,7 @@ public class MessageUpdate implements Serializable {
*/
@Nullable
public Uri getMediaUri() {
return Uri.parse(uriString);
return Uri.parse(mediaUri);
}
/**
@ -98,14 +96,11 @@ public class MessageUpdate implements Serializable {
DocumentFile file = DocumentFile.fromSingleUri(context, uri);
String mime = context.getContentResolver().getType(uri);
// check if file is valid
if (file == null || file.length() == 0) {
if (mime == null || file == null || file.length() == 0 || !supportedFormats.contains(mime)) {
return false;
}
// check if file format is supported
if (mime == null || !supportedFormats.contains(mime)) {
return false;
}
this.uriString = uri.toString();
this.mediaUri = uri.toString();
mediaUpdate = new MediaStatus(uri.toString(), mime);
return true;
}
@ -115,22 +110,7 @@ public class MessageUpdate implements Serializable {
* @return true if initialization succeded
*/
public boolean prepare(ContentResolver resolver) {
if (uriString == null) {
// no need to check media files if not attached
return true;
}
try {
Uri uri = Uri.parse(uriString);
String mimeType = resolver.getType(uri);
InputStream fileStream = resolver.openInputStream(uri);
if (fileStream != null && mimeType != null && fileStream.available() > 0) {
mediaUpdate = new MediaStatus(fileStream, mimeType);
return true;
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
return mediaUpdate == null || mediaUpdate.openStream(resolver);
}
/**
@ -163,6 +143,6 @@ public class MessageUpdate implements Serializable {
@NonNull
@Override
public String toString() {
return "to=\"" + name + "\" text=\"" + text + "\" media=" + (mediaUpdate != null);
return "to=\"" + name + "\" text=\"" + text + "\" media=" + mediaUpdate;
}
}

View File

@ -10,9 +10,9 @@ import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import org.nuclearfog.twidda.model.Instance;
import org.nuclearfog.twidda.model.Media;
import org.nuclearfog.twidda.model.Status;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
@ -76,18 +76,20 @@ public class StatusUpdate implements Serializable {
private PollUpdate poll;
@Nullable
private LocationUpdate location;
private MediaStatus[] mediaUpdates = {};
private int attachment = EMPTY;
// helper attributes
@Nullable
private Instance instance;
private List<String> mediaUriStrings = new ArrayList<>(5);
private List<String> previews = new ArrayList<>();
private List<String> mediaKeys = new ArrayList<>();
private List<MediaStatus> mediaStatuses = new ArrayList<>();
private Set<String> supportedFormats = new TreeSet<>();
private boolean attachmentLimitReached = false;
/**
* set existing status to edit
* set informations of an existing status to edit these
*
* @param status existing status
*/
@ -106,6 +108,36 @@ public class StatusUpdate implements Serializable {
}
}
/**
* set information media files attached to an existing status
*
* @param status status contianing media
* @return attached media type {@link #EMPTY,#MEDIA_VIDEO,#MEDIA_IMAGE,#MEDIA_GIF}
*/
public int setMedia(Status status) {
if (status.getMedia().length > 0) {
for (Media media : status.getMedia()) {
mediaKeys.add(media.getKey());
previews.add(media.getUrl());
}
attachmentLimitReached = true;
switch (status.getMedia()[0].getMediaType()) {
case Media.GIF:
attachment = MEDIA_GIF;
break;
case Media.PHOTO:
attachment = MEDIA_IMAGE;
break;
case Media.VIDEO:
attachment = MEDIA_VIDEO;
break;
}
}
return attachment;
}
/**
* set ID of the replied status
*
@ -117,6 +149,8 @@ public class StatusUpdate implements Serializable {
/**
* add status text
*
* @param text status text
*/
public void addText(String text) {
this.text = text;
@ -142,8 +176,9 @@ public class StatusUpdate implements Serializable {
case MEDIA_GIF:
DocumentFile file = DocumentFile.fromSingleUri(context, mediaUri);
if (file != null && file.length() > 0) {
mediaUriStrings.add(mediaUri.toString());
if (mediaUriStrings.size() == instance.getGifLimit()) {
previews.add(mediaUri.toString());
mediaStatuses.add(new MediaStatus(mediaUri.toString(), mime));
if (mediaStatuses.size() == instance.getGifLimit()) {
attachmentLimitReached = true;
}
return MEDIA_GIF;
@ -161,8 +196,9 @@ public class StatusUpdate implements Serializable {
case MEDIA_IMAGE:
DocumentFile file = DocumentFile.fromSingleUri(context, mediaUri);
if (file != null && file.length() > 0) {
mediaUriStrings.add(mediaUri.toString());
if (mediaUriStrings.size() == instance.getImageLimit()) {
previews.add(mediaUri.toString());
mediaStatuses.add(new MediaStatus(mediaUri.toString(), mime));
if (mediaStatuses.size() == instance.getImageLimit()) {
attachmentLimitReached = true;
}
return MEDIA_IMAGE;
@ -179,8 +215,9 @@ public class StatusUpdate implements Serializable {
case MEDIA_VIDEO:
DocumentFile file = DocumentFile.fromSingleUri(context, mediaUri);
if (file != null && file.length() > 0) {
mediaUriStrings.add(mediaUri.toString());
if (mediaUriStrings.size() == instance.getVideoLimit()) {
previews.add(mediaUri.toString());
mediaStatuses.add(new MediaStatus(mediaUri.toString(), mime));
if (mediaStatuses.size() == instance.getVideoLimit()) {
attachmentLimitReached = true;
}
return MEDIA_VIDEO;
@ -299,8 +336,8 @@ public class StatusUpdate implements Serializable {
*
* @return list of media updates
*/
public MediaStatus[] getMediaUpdates() {
return mediaUpdates;
public List<MediaStatus> getMediaStatuses() {
return new ArrayList<>(mediaStatuses);
}
/**
@ -309,9 +346,9 @@ public class StatusUpdate implements Serializable {
* @return media uri array
*/
public Uri[] getMediaUris() {
Uri[] result = new Uri[mediaUriStrings.size()];
Uri[] result = new Uri[previews.size()];
for (int i = 0 ; i < result.length ; i++) {
result[i] = Uri.parse(mediaUriStrings.get(i));
result[i] = Uri.parse(previews.get(i));
}
return result;
}
@ -336,6 +373,15 @@ public class StatusUpdate implements Serializable {
return location;
}
/**
* get media keys (IDs) of online media
*
* @return media key
*/
public String[] getMediaKeys() {
return mediaKeys.toArray(new String[0]);
}
/**
* @return true if status content is sensitive
*/
@ -382,7 +428,7 @@ public class StatusUpdate implements Serializable {
* @return true if media is attached
*/
public boolean isEmpty() {
return mediaUriStrings.isEmpty() && location == null && poll == null && getText() == null;
return previews.isEmpty() && location == null && poll == null && getText() == null;
}
/**
@ -391,21 +437,16 @@ public class StatusUpdate implements Serializable {
* @return true if success, false if an error occurs
*/
public boolean prepare(ContentResolver resolver) {
if (mediaUriStrings.isEmpty())
if (previews.isEmpty())
return true;
try {
// open input streams
mediaUpdates = new MediaStatus[mediaUriStrings.size()];
for (int i = 0; i < mediaUpdates.length; i++) {
Uri uri = Uri.parse(mediaUriStrings.get(i));
InputStream is = resolver.openInputStream(uri);
String mime = resolver.getType(uri);
// check if stream is valid
if (is != null && mime != null && is.available() > 0) {
mediaUpdates[i] = new MediaStatus(is, mime);
} else {
for (MediaStatus mediaStatus : mediaStatuses) {
boolean success = mediaStatus.openStream(resolver);
if (!success) {
return false;
}
}
} catch (Exception e) {
e.printStackTrace();
@ -418,8 +459,10 @@ public class StatusUpdate implements Serializable {
* close all open streams
*/
public void close() {
for (MediaStatus mediaUpdate : mediaUpdates) {
mediaUpdate.close();
for (MediaStatus mediaUpdate : mediaStatuses) {
if (mediaUpdate != null) {
mediaUpdate.close();
}
}
}

View File

@ -1,8 +1,9 @@
package org.nuclearfog.twidda.backend.helper;
import androidx.annotation.Nullable;
import org.nuclearfog.twidda.model.Status;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
@ -30,6 +31,17 @@ public class Statuses extends LinkedList<Status> {
super();
}
/**
* create a copy of statuses
*
* @param statuses list to copy
*/
public Statuses(Statuses statuses) {
super(statuses);
this.minId = statuses.getMinId();
this.maxId = statuses.getMaxId();
}
/**
* @param minId minimum ID of the first item
* @param maxId maximum ID of the last item
@ -40,6 +52,15 @@ public class Statuses extends LinkedList<Status> {
this.maxId = maxId;
}
/**
* @inheritDoc
*/
@Nullable
@Override
public Status get(int index) {
return super.get(index);
}
/**
* get the minimum ID of this list. If not set, use the first item's ID
*
@ -114,16 +135,4 @@ public class Statuses extends LinkedList<Status> {
minId = statuses.getMinId();
maxId = statuses.getMaxId();
}
/**
* replace all items with new ones
*
* @param statuses new items to insert
*/
public void replaceAll(Status[] statuses) {
clear();
addAll(Arrays.asList(statuses));
minId = statuses[0].getId();
maxId = statuses[statuses.length - 1].getId();
}
}

View File

@ -143,6 +143,9 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
preferenceDialog = new StatusPreferenceDialog(this, statusUpdate);
pollDialog = new PollDialog(this, this);
emojiPicker = new EmojiPicker(this, this);
adapter = new IconAdapter(settings, true);
iconList.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, true));
iconList.setAdapter(adapter);
AppStyles.setEditorTheme(root, background);
if (!settings.getLogin().getConfiguration().locationSupported()) {
@ -166,6 +169,10 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
if (editStatus) {
statusUpdate.setStatus(status);
statusText.append(status.getText());
if (status.getMedia().length > 0) {
int mediaType = statusUpdate.setMedia(status);
addMedia(mediaType);
}
} else {
statusUpdate.addReplyStatusId(status.getId());
statusUpdate.setVisibility(status.getVisibility());
@ -177,11 +184,8 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
statusText.append(prefix);
}
}
adapter = new IconAdapter(settings, true);
adapter.addOnMediaClickListener(this);
iconList.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, true));
iconList.setAdapter(adapter);
adapter.addOnMediaClickListener(this);
statusText.addTextChangedListener(this);
emojiButton.setOnClickListener(this);
preference.setOnClickListener(this);
@ -332,31 +336,7 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
@Override
protected void onMediaFetched(int resultType, @NonNull Uri uri) {
int mediaType = statusUpdate.addMedia(this, uri);
switch (mediaType) {
case StatusUpdate.MEDIA_IMAGE:
adapter.addImageItem();
break;
case StatusUpdate.MEDIA_GIF:
adapter.addGifItem();
break;
case StatusUpdate.MEDIA_VIDEO:
adapter.addVideoItem();
break;
case StatusUpdate.MEDIA_ERROR:
Toast.makeText(getApplicationContext(), R.string.error_adding_media, Toast.LENGTH_SHORT).show();
break;
}
// hide media button if limit is reached
if (statusUpdate.mediaLimitReached()) {
mediaBtn.setVisibility(View.GONE);
}
// hide poll button
if (mediaType != StatusUpdate.MEDIA_ERROR && pollBtn.getVisibility() != View.GONE) {
pollBtn.setVisibility(View.GONE);
}
addMedia(mediaType);
}
@ -423,6 +403,34 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
}
}
private void addMedia(int mediaType) {
switch (mediaType) {
case StatusUpdate.MEDIA_IMAGE:
adapter.addImageItem();
break;
case StatusUpdate.MEDIA_GIF:
adapter.addGifItem();
break;
case StatusUpdate.MEDIA_VIDEO:
adapter.addVideoItem();
break;
case StatusUpdate.MEDIA_ERROR:
Toast.makeText(getApplicationContext(), R.string.error_adding_media, Toast.LENGTH_SHORT).show();
break;
}
// hide media button if limit is reached
if (statusUpdate.mediaLimitReached()) {
mediaBtn.setVisibility(View.GONE);
}
// hide poll button
if (mediaType != StatusUpdate.MEDIA_ERROR && pollBtn.getVisibility() != View.GONE) {
pollBtn.setVisibility(View.GONE);
}
}
/**
* called when the status was successfully updated
*/

View File

@ -189,7 +189,7 @@ public class MessageAdapter extends Adapter<ViewHolder> implements OnItemClickLi
public void replaceItems(Messages newMessages) {
messages.clear();
messages.replaceAll(newMessages);
if (newMessages.getNextCursor() != null && !newMessages.getNextCursor().isEmpty()) {
if (newMessages.getNextCursor() != null && !newMessages.getNextCursor().isEmpty() && messages.peekLast() != null) {
// add placeholder
messages.add(null);
}

View File

@ -150,8 +150,8 @@ public class StatusAdapter extends Adapter<ViewHolder> implements OnHolderClickL
*
* @return item array
*/
public Status[] getItems() {
return items.toArray(new Status[0]);
public Statuses getItems() {
return new Statuses(items);
}
/**
@ -186,20 +186,7 @@ public class StatusAdapter extends Adapter<ViewHolder> implements OnHolderClickL
*/
public void replaceItems(@NonNull Statuses newItems) {
items.replaceAll(newItems);
if (items.size() > MIN_COUNT && items.getMaxId() != Statuses.NO_ID)
items.add(null);
loadingIndex = NO_LOADING;
notifyDataSetChanged();
}
/**
* Replace all items in the list
*
* @param newItems array of statuses to add
*/
public void replaceItems(Status[] newItems) {
items.replaceAll(newItems);
if (items.size() > MIN_COUNT)
if (items.size() > MIN_COUNT && items.getMaxId() != Statuses.NO_ID && items.peekLast() != null)
items.add(null);
loadingIndex = NO_LOADING;
notifyDataSetChanged();
@ -252,7 +239,10 @@ public class StatusAdapter extends Adapter<ViewHolder> implements OnHolderClickL
*/
public long getTopItemId() {
if (!items.isEmpty() && items.get(0) != null) {
return items.get(0).getId();
Status status = items.get(0);
if (status != null) {
return status.getId();
}
}
return 0L;
}

View File

@ -175,7 +175,7 @@ public class UserAdapter extends Adapter<ViewHolder> implements OnHolderClickLis
*/
public void replaceItems(Users newUsers) {
users.replaceAll(newUsers);
if (users.getNext() != 0L) {
if (users.getNext() != 0L && users.peekLast() != null) {
users.add(null);
}
notifyDataSetChanged();

View File

@ -177,7 +177,7 @@ public class UserlistAdapter extends Adapter<ViewHolder> implements OnHolderClic
*/
public void replaceItems(UserLists newUserlists) {
userlists.replaceAll(newUserlists);
if (userlists.getNext() != 0L) {
if (userlists.getNext() != 0L && userlists.peekLast() != null) {
// Add placeholder
userlists.add(null);
}

View File

@ -18,6 +18,7 @@ import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback;
import org.nuclearfog.twidda.backend.async.StatusLoader;
import org.nuclearfog.twidda.backend.async.StatusLoader.StatusParameter;
import org.nuclearfog.twidda.backend.async.StatusLoader.StatusResult;
import org.nuclearfog.twidda.backend.helper.Statuses;
import org.nuclearfog.twidda.backend.utils.ErrorHandler;
import org.nuclearfog.twidda.model.Status;
import org.nuclearfog.twidda.ui.activities.StatusActivity;
@ -142,8 +143,8 @@ public class StatusFragment extends ListFragment implements StatusSelectListener
}
if (savedInstanceState != null) {
Serializable data = savedInstanceState.getSerializable(KEY_STATUS_FRAGMENT_SAVE);
if (data instanceof Status[]) {
adapter.replaceItems((Status[]) data);
if (data instanceof Statuses) {
adapter.replaceItems((Statuses) data);
}
}
setAdapter(adapter);