M src/cheogram/java/com/cheogram/android/BobTransfer.java => src/cheogram/java/com/cheogram/android/BobTransfer.java +51 -22
@@ 15,6 15,7 @@ import io.ipfs.cid.Cid;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
@@ 24,15 25,18 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
public class BobTransfer implements Transferable {
protected int status = Transferable.STATUS_OFFER;
- protected Message message;
protected URI uri;
+ protected Account account;
+ protected Jid to;
protected XmppConnectionService xmppConnectionService;
public static Cid cid(URI uri) {
+ if (!uri.getScheme().equals("cid")) return null;
String bobCid = uri.getSchemeSpecificPart();
if (!bobCid.contains("@") || !bobCid.contains("+")) return null;
String[] cidParts = bobCid.split("@")[0].split("\\+");
@@ 43,10 47,15 @@ public class BobTransfer implements Transferable {
}
}
- public BobTransfer(Message message, XmppConnectionService xmppConnectionService) throws URISyntaxException {
- this.message = message;
+ public static URI uri(Cid cid) throws NoSuchAlgorithmException, URISyntaxException {
+ return new URI("cid", CryptoHelper.multihashAlgo(cid.getType()) + "+" + CryptoHelper.bytesToHex(cid.getHash()) + "@bob.xmpp.org", null);
+ }
+
+ public BobTransfer(URI uri, Account account, Jid to, XmppConnectionService xmppConnectionService) {
this.xmppConnectionService = xmppConnectionService;
- this.uri = new URI(message.getFileParams().url);
+ this.uri = uri;
+ this.to = to;
+ this.account = account;
}
@Override
@@ 55,10 64,7 @@ public class BobTransfer implements Transferable {
File f = xmppConnectionService.getFileForCid(cid(uri));
if (f != null && f.canRead()) {
- message.setRelativeFilePath(f.getAbsolutePath());
- finish();
- message.setTransferable(null);
- xmppConnectionService.updateConversationUi();
+ finish(f);
return true;
}
@@ 66,13 72,14 @@ public class BobTransfer implements Transferable {
changeStatus(Transferable.STATUS_DOWNLOADING);
IqPacket request = new IqPacket(IqPacket.TYPE.GET);
- request.setTo(message.getCounterpart());
+ request.setTo(to);
final Element dataq = request.addChild("data", "urn:xmpp:bob");
dataq.setAttribute("cid", uri.getSchemeSpecificPart());
- xmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (acct, packet) -> {
+ xmppConnectionService.sendIqPacket(account, request, (acct, packet) -> {
final Element data = packet.findChild("data", "urn:xmpp:bob");
if (packet.getType() == IqPacket.TYPE.ERROR || data == null) {
Log.d(Config.LOGTAG, "BobTransfer failed: " + packet);
+ finish(null);
xmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
} else {
final String contentType = data.getAttribute("type");
@@ 84,25 91,23 @@ public class BobTransfer implements Transferable {
try {
final byte[] bytes = Base64.decode(data.getContent(), Base64.DEFAULT);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message, new ByteArrayInputStream(bytes), fileExtension);
- DownloadableFile file = xmppConnectionService.getFileBackend().getFile(message);
+ File file = xmppConnectionService.getFileBackend().getStorageLocation(new ByteArrayInputStream(bytes), fileExtension);
file.getParentFile().mkdirs();
if (!file.exists() && !file.createNewFile()) {
throw new IOException(file.getAbsolutePath());
}
- final OutputStream outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
+ final OutputStream outputStream = AbstractConnectionManager.createOutputStream(new DownloadableFile(file.getAbsolutePath()), false, false);
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
- finish();
+ finish(file);
} catch (IOException e) {
+ finish(null);
xmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
}
}
- message.setTransferable(null);
- xmppConnectionService.updateConversationUi();
});
return true;
} else {
@@ 129,7 134,6 @@ public class BobTransfer implements Transferable {
public void cancel() {
// No real way to cancel an iq in process...
changeStatus(Transferable.STATUS_CANCELLED);
- message.setTransferable(null);
}
protected void changeStatus(int newStatus) {
@@ 137,10 141,35 @@ public class BobTransfer implements Transferable {
xmppConnectionService.updateConversationUi();
}
- protected void finish() {
- final boolean privateMessage = message.isPrivateMessage();
- message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
- xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString(), false);
- xmppConnectionService.updateMessage(message);
+ protected void finish(File f) {
+ if (f != null) xmppConnectionService.updateConversationUi();
+ }
+
+ public static class ForMessage extends BobTransfer {
+ protected Message message;
+
+ public ForMessage(Message message, XmppConnectionService xmppConnectionService) throws URISyntaxException {
+ super(new URI(message.getFileParams().url), message.getConversation().getAccount(), message.getCounterpart(), xmppConnectionService);
+ this.message = message;
+ }
+
+ @Override
+ public void cancel() {
+ super.cancel();
+ message.setTransferable(null);
+ }
+
+ @Override
+ protected void finish(File f) {
+ if (f != null) {
+ message.setRelativeFilePath(f.getAbsolutePath());
+ final boolean privateMessage = message.isPrivateMessage();
+ message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
+ xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString(), false);
+ xmppConnectionService.updateMessage(message);
+ }
+ message.setTransferable(null);
+ super.finish(f);
+ }
}
}
A src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java => src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java +9 -0
@@ 0,0 1,9 @@
+package com.cheogram.android;
+
+import android.graphics.drawable.Drawable;
+
+import io.ipfs.cid.Cid;
+
+public interface GetThumbnailForCid {
+ public Drawable getThumbnail(Cid cid);
+}
M src/main/java/eu/siacs/conversations/entities/Contact.java => src/main/java/eu/siacs/conversations/entities/Contact.java +4 -0
@@ 414,6 414,10 @@ public class Contact implements ListItem, Blockable {
return ((this.subscription & (1 << option)) != 0);
}
+ public boolean canInferPresence() {
+ return showInContactList() || isSelf();
+ }
+
public boolean showInRoster() {
return (this.getOption(Contact.Options.IN_ROSTER) && (!this
.getOption(Contact.Options.DIRTY_DELETE)))
M src/main/java/eu/siacs/conversations/entities/Conversation.java => src/main/java/eu/siacs/conversations/entities/Conversation.java +6 -0
@@ 1130,6 1130,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return count;
}
+ public boolean canInferPresence() {
+ final Contact contact = getContact();
+ if (contact != null && contact.canInferPresence()) return true;
+ return sentMessagesCount() > 0;
+ }
+
public boolean isWithStranger() {
final Contact contact = getContact();
return mode == MODE_SINGLE
M src/main/java/eu/siacs/conversations/entities/Conversational.java => src/main/java/eu/siacs/conversations/entities/Conversational.java +2 -0
@@ 45,4 45,6 @@ public interface Conversational {
int getMode();
String getUuid();
+
+ boolean canInferPresence();
}
M src/main/java/eu/siacs/conversations/entities/Message.java => src/main/java/eu/siacs/conversations/entities/Message.java +55 -2
@@ 2,10 2,15 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
+import android.graphics.drawable.Drawable;
import android.graphics.Color;
+import android.text.Html;
import android.text.SpannableStringBuilder;
import android.util.Log;
+import com.cheogram.android.BobTransfer;
+import com.cheogram.android.GetThumbnailForCid;
+
import com.google.common.io.ByteSource;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
@@ 25,6 30,8 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.concurrent.CopyOnWriteArraySet;
+import io.ipfs.cid.Cid;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
@@ 762,8 769,42 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public static class MergeSeparator {
}
+ public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
+ final Element html = getHtml();
+ if (html == null) {
+ return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim());
+ } else {
+ SpannableStringBuilder spannable = new SpannableStringBuilder(Html.fromHtml(
+ MessageUtils.filterLtrRtl(html.toString()).trim(),
+ Html.FROM_HTML_MODE_COMPACT,
+ (source) -> {
+ try {
+ if (thumbnailer == null) return fallbackImg;
+ Cid cid = BobTransfer.cid(new URI(source));
+ if (cid == null) return fallbackImg;
+ Drawable thumbnail = thumbnailer.getThumbnail(cid);
+ if (thumbnail == null) return fallbackImg;
+ return thumbnail;
+ } catch (final URISyntaxException e) {
+ return fallbackImg;
+ }
+ },
+ (opening, tag, output, xmlReader) -> {}
+ ));
+
+ // https://stackoverflow.com/a/10187511/8611
+ int i = spannable.length();
+ while(--i >= 0 && Character.isWhitespace(spannable.charAt(i))) { }
+ return (SpannableStringBuilder) spannable.subSequence(0, i+1);
+ }
+ }
+
public SpannableStringBuilder getMergedBody() {
- SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim());
+ return getMergedBody(null, null);
+ }
+
+ public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
+ SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg);
Message current = this;
while (current.mergeable(current.next())) {
current = current.next();
@@ 773,7 814,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
body.append("\n\n");
body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
- body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
+ body.append(current.getSpannableBody(thumbnailer, fallbackImg));
}
return body;
}
@@ 868,6 909,18 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.payloads.add(el);
}
+ public Element getHtml() {
+ if (this.payloads == null) return null;
+
+ for (Element el : this.payloads) {
+ if (el.getName().equals("html") && el.getNamespace().equals("http://jabber.org/protocol/xhtml-im")) {
+ return el.getChildren().get(0);
+ }
+ }
+
+ return null;
+ }
+
public List<Element> getCommands() {
if (this.payloads == null) return null;
M src/main/java/eu/siacs/conversations/entities/StubConversation.java => src/main/java/eu/siacs/conversations/entities/StubConversation.java +6 -0
@@ 70,4 70,10 @@ public class StubConversation implements Conversational {
public String getUuid() {
return uuid;
}
+
+ @Override
+ public boolean canInferPresence() {
+ final Contact contact = getContact();
+ return contact != null && contact.canInferPresence();
+ }
}
M src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java => src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +1 -0
@@ 112,6 112,7 @@ public abstract class AbstractGenerator {
public List<String> getFeatures(Account account) {
final XmppConnection connection = account.getXmppConnection();
final ArrayList<String> features = new ArrayList<>(Arrays.asList(FEATURES));
+ features.add("http://jabber.org/protocol/xhtml-im");
if (mXmppConnectionService.confirmMessages()) {
features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES));
}
M src/main/java/eu/siacs/conversations/parser/MessageParser.java => src/main/java/eu/siacs/conversations/parser/MessageParser.java +10 -4
@@ 434,6 434,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
boolean notify = false;
+ Element html = original.findChild("html", "http://jabber.org/protocol/xhtml-im");
+ if (html != null && html.findChild("body", "http://www.w3.org/1999/xhtml") == null) {
+ html = null;
+ }
+
if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) {
Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
return;
@@ 472,7 477,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
- if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) {
+ if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || html != null) && !isMucStatusMessage) {
final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString());
final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
@@ 577,12 582,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
}
} else {
- message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
- if (body.count > 1) {
+ message = new Message(conversation, body == null ? "HTML-only message" : body.content, Message.ENCRYPTION_NONE, status);
+ if (body != null && body.count > 1) {
message.setBodyLanguage(body.language);
}
}
+ if (html != null) message.addPayload(html);
message.setSubject(original.findChildContent("subject"));
message.setCounterpart(counterpart);
message.setRemoteMsgId(remoteMsgId);
@@ 759,7 765,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
if (message.getOob() != null && message.getOob().getScheme().equalsIgnoreCase("cid")) {
try {
- BobTransfer transfer = new BobTransfer(message, mXmppConnectionService);
+ BobTransfer transfer = new BobTransfer.ForMessage(message, mXmppConnectionService);
message.setTransferable(transfer);
transfer.start();
} catch (URISyntaxException e) {
M src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java => src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +4 -3
@@ 48,6 48,7 @@ import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.entities.Roster;
@@ 733,12 734,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
cursor.close();
}
- public File getFileForCid(Cid cid) {
+ public DownloadableFile getFileForCid(Cid cid) {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query("cheogram.cids", new String[]{"path"}, "cid=?", new String[]{cid.toString()}, null, null, null);
- File f = null;
+ DownloadableFile f = null;
if (cursor.moveToNext()) {
- f = new File(cursor.getString(0));
+ f = new DownloadableFile(cursor.getString(0));
}
cursor.close();
return f;
M src/main/java/eu/siacs/conversations/persistance/FileBackend.java => src/main/java/eu/siacs/conversations/persistance/FileBackend.java +38 -22
@@ 13,6 13,7 @@ import android.graphics.drawable.Drawable;
import android.graphics.ImageDecoder;
import android.graphics.Matrix;
import android.graphics.Paint;
+import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.pdf.PdfRenderer;
import android.media.MediaMetadataRetriever;
@@ 886,13 887,7 @@ public class FileBackend {
}
public void setupRelativeFilePath(final Message message, final InputStream is, final String extension) throws IOException {
- Cid[] cids = calculateCids(is);
-
- setupRelativeFilePath(message, String.format("%s.%s", cids[0], extension));
- File file = getFile(message);
- for (int i = 0; i < cids.length; i++) {
- mXmppConnectionService.saveCid(cids[i], file);
- }
+ message.setRelativeFilePath(getStorageLocation(is, extension).getAbsolutePath());
}
public void setupRelativeFilePath(final Message message, final String filename) {
@@ 901,6 896,17 @@ public class FileBackend {
setupRelativeFilePath(message, filename, mime);
}
+ public File getStorageLocation(final InputStream is, final String extension) throws IOException {
+ final String mime = MimeUtils.guessMimeTypeFromExtension(extension);
+ Cid[] cids = calculateCids(is);
+
+ File file = getStorageLocation(String.format("%s.%s", cids[0], extension), mime);
+ for (int i = 0; i < cids.length; i++) {
+ mXmppConnectionService.saveCid(cids[i], file);
+ }
+ return file;
+ }
+
public File getStorageLocation(final String filename, final String mime) {
final File parentDirectory;
if (Strings.isNullOrEmpty(mime)) {
@@ 995,16 1001,18 @@ public class FileBackend {
}
public Drawable getThumbnail(Message message, Resources res, int size, boolean cacheOnly) throws IOException {
- final String uuid = message.getUuid();
+ return getThumbnail(getFile(message), res, size, cacheOnly);
+ }
+
+ public Drawable getThumbnail(DownloadableFile file, Resources res, int size, boolean cacheOnly) throws IOException {
final LruCache<String, Drawable> cache = mXmppConnectionService.getDrawableCache();
- Drawable thumbnail = cache.get(uuid);
+ Drawable thumbnail = cache.get(file.getAbsolutePath());
if ((thumbnail == null) && (!cacheOnly)) {
synchronized (THUMBNAIL_LOCK) {
- thumbnail = cache.get(uuid);
+ thumbnail = cache.get(file.getAbsolutePath());
if (thumbnail != null) {
return thumbnail;
}
- DownloadableFile file = getFile(message);
final String mime = file.getMimeType();
if ("application/pdf".equals(mime)) {
thumbnail = new BitmapDrawable(res, getPdfDocumentPreview(file, size));
@@ 1016,7 1024,7 @@ public class FileBackend {
throw new FileNotFoundException();
}
}
- cache.put(uuid, thumbnail);
+ cache.put(file.getAbsolutePath(), thumbnail);
}
}
return thumbnail;
@@ 1028,22 1036,30 @@ public class FileBackend {
return drawDrawable(drawable);
}
+ public static Rect rectForSize(int w, int h, int size) {
+ int scalledW;
+ int scalledH;
+ if (w <= h) {
+ scalledW = Math.max((int) (w / ((double) h / size)), 1);
+ scalledH = size;
+ } else {
+ scalledW = size;
+ scalledH = Math.max((int) (h / ((double) w / size)), 1);
+ }
+
+ if (scalledW > w || scalledH > h) return new Rect(0, 0, w, h);
+
+ return new Rect(0, 0, scalledW, scalledH);
+ }
+
private Drawable getImagePreview(File file, Resources res, int size, final String mime) throws IOException {
if (android.os.Build.VERSION.SDK_INT >= 28) {
ImageDecoder.Source source = ImageDecoder.createSource(file);
return ImageDecoder.decodeDrawable(source, (decoder, info, src) -> {
int w = info.getSize().getWidth();
int h = info.getSize().getHeight();
- int scalledW;
- int scalledH;
- if (w <= h) {
- scalledW = Math.max((int) (w / ((double) h / size)), 1);
- scalledH = size;
- } else {
- scalledW = size;
- scalledH = Math.max((int) (h / ((double) w / size)), 1);
- }
- decoder.setTargetSize(scalledW, scalledH);
+ Rect r = rectForSize(w, h, size);
+ decoder.setTargetSize(r.width(), r.height());
});
} else {
BitmapFactory.Options options = new BitmapFactory.Options();
M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +2 -1
@@ 103,6 103,7 @@ import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
+import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
@@ 549,7 550,7 @@ public class XmppConnectionService extends Service {
return this.fileBackend;
}
- public File getFileForCid(Cid cid) {
+ public DownloadableFile getFileForCid(Cid cid) {
return this.databaseBackend.getFileForCid(cid);
}
M src/main/java/eu/siacs/conversations/ui/ConversationFragment.java => src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +1 -1
@@ 1946,7 1946,7 @@ public class ConversationFragment extends XmppFragment
}
if (message.getOob() != null && message.getOob().getScheme().equalsIgnoreCase("cid")) {
try {
- BobTransfer transfer = new BobTransfer(message, activity.xmppConnectionService);
+ BobTransfer transfer = new BobTransfer.ForMessage(message, activity.xmppConnectionService);
message.setTransferable(transfer);
transfer.start();
} catch (URISyntaxException e) {
M src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java => src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +60 -1
@@ 6,8 6,10 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
import android.graphics.Typeface;
import android.net.Uri;
+import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.text.Spannable;
import android.text.SpannableString;
@@ 32,15 34,23 @@ import android.widget.Toast;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
+import androidx.core.content.res.ResourcesCompat;
+
+import com.cheogram.android.BobTransfer;
import com.google.common.base.Strings;
+import java.io.IOException;
import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import io.ipfs.cid.Cid;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
@@ 439,7 449,32 @@ public class MessageAdapter extends ArrayAdapter<Message> {
if (message.getBody() != null && !message.getBody().equals("")) {
viewHolder.messageBody.setVisibility(View.VISIBLE);
final String nick = UIHelper.getMessageDisplayName(message);
- SpannableStringBuilder body = message.getMergedBody();
+ Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), activity.getThemeResource(R.attr.ic_attach_photo, R.drawable.ic_attach_photo), null);
+ fallbackImg.setBounds(FileBackend.rectForSize(fallbackImg.getIntrinsicWidth(), fallbackImg.getIntrinsicHeight(), (int) (metrics.density * 32)));
+ SpannableStringBuilder body = message.getMergedBody((cid) -> {
+ try {
+ DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
+ if (f == null || !f.canRead()) {
+ if (!message.trusted() && !message.getConversation().canInferPresence()) return null;
+
+ try {
+ new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
+ } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
+ return null;
+ }
+
+ Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true);
+ if (d == null) {
+ new ThumbnailTask().execute(f);
+ } else {
+ d = d.getConstantState().newDrawable();
+ d.setBounds(FileBackend.rectForSize(d.getIntrinsicWidth(), d.getIntrinsicHeight(), (int) (metrics.density * 32)));
+ }
+ return d;
+ } catch (final IOException e) {
+ return fallbackImg;
+ }
+ }, fallbackImg);
boolean hasMeCommand = message.hasMeCommand();
if (hasMeCommand) {
body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
@@ 959,4 994,28 @@ public class MessageAdapter extends ArrayAdapter<Message> {
protected TextView encryption;
protected ListView commands_list;
}
+
+ class ThumbnailTask extends AsyncTask<DownloadableFile, Void, Drawable[]> {
+ @Override
+ protected Drawable[] doInBackground(DownloadableFile... params) {
+ if (isCancelled()) return null;
+
+ Drawable[] d = new Drawable[params.length];
+ for (int i = 0; i < params.length; i++) {
+ try {
+ d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false);
+ } catch (final IOException e) {
+ d[i] = null;
+ }
+ }
+
+ return d;
+ }
+
+ @Override
+ protected void onPostExecute(final Drawable[] d) {
+ if (isCancelled()) return;
+ activity.xmppConnectionService.updateConversationUi();
+ }
+ }
}
M src/main/java/eu/siacs/conversations/utils/CryptoHelper.java => src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +13 -0
@@ 288,6 288,19 @@ public final class CryptoHelper {
return !u.contains(" ") && (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("p1s3://")) && u.endsWith(".pgp");
}
+ public static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException {
+ switch(type) {
+ case sha1:
+ return "sha1";
+ case sha2_256:
+ return "sha-256";
+ case sha2_512:
+ return "sha-512";
+ default:
+ throw new NoSuchAlgorithmException("" + type);
+ }
+ }
+
public static Multihash.Type multihashType(String algo) throws NoSuchAlgorithmException {
if (algo.equals("SHA-1") || algo.equals("sha-1") || algo.equals("sha1")) {
return Multihash.Type.sha1;
M src/main/java/eu/siacs/conversations/utils/UIHelper.java => src/main/java/eu/siacs/conversations/utils/UIHelper.java +5 -1
@@ 1,12 1,14 @@
package eu.siacs.conversations.utils;
import android.content.Context;
+import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Pair;
import androidx.annotation.ColorInt;
+import androidx.core.content.res.ResourcesCompat;
import com.google.common.base.Strings;
@@ 319,7 321,9 @@ public class UIHelper {
return new Pair<>(context.getString(R.string.x_file_offered_for_download,
getFileDescriptionString(context, message)), true);
} else {
- SpannableStringBuilder styledBody = new SpannableStringBuilder(body);
+ Drawable fallbackImg = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_attach_photo, null);
+ fallbackImg.setBounds(0, 0, fallbackImg.getIntrinsicWidth(), fallbackImg.getIntrinsicHeight());
+ SpannableStringBuilder styledBody = message.getSpannableBody(null, fallbackImg);
if (textColor != 0) {
StylingHelper.format(styledBody, 0, styledBody.length() - 1, textColor);
}
M src/main/java/eu/siacs/conversations/xml/Element.java => src/main/java/eu/siacs/conversations/xml/Element.java +41 -19
@@ 3,19 3,21 @@ package eu.siacs.conversations.xml;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
+import java.util.stream.Collectors;
import eu.siacs.conversations.utils.XmlHelper;
import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
-public class Element {
+public class Element implements Node {
private final String name;
private Hashtable<String, String> attributes = new Hashtable<>();
- private String content;
- protected List<Element> children = new ArrayList<>();
+ private List<Element> children = new ArrayList<>();
+ private List<Node> childNodes = new ArrayList<>();
public Element(String name) {
this.name = name;
@@ 26,30 28,52 @@ public class Element {
this.setAttribute("xmlns", xmlns);
}
- public Element addChild(Element child) {
- this.content = null;
- children.add(child);
+ public Node prependChild(Node child) {
+ childNodes.add(0, child);
+ if (child instanceof Element) children.add(0, (Element) child);
+ return child;
+ }
+
+ public Node addChild(Node child) {
+ childNodes.add(child);
+ if (child instanceof Element) children.add((Element) child);
return child;
}
public Element addChild(String name) {
- this.content = null;
Element child = new Element(name);
+ childNodes.add(child);
children.add(child);
return child;
}
public Element addChild(String name, String xmlns) {
- this.content = null;
Element child = new Element(name);
child.setAttribute("xmlns", xmlns);
+ childNodes.add(child);
children.add(child);
return child;
}
+ public void addChildren(final Collection<? extends Node> children) {
+ if (children == null) return;
+
+ this.childNodes.addAll(children);
+ for (Node node : children) {
+ if (node instanceof Element) {
+ this.children.add((Element) node);
+ }
+ }
+ }
+
+ public void removeChild(Node child) {
+ this.childNodes.remove(child);
+ if (child instanceof Element) this.children.remove(child);
+ }
+
public Element setContent(String content) {
- this.content = content;
- this.children.clear();
+ clearChildren();
+ if (content != null) this.childNodes.add(new TextNode(content));
return this;
}
@@ 106,17 130,18 @@ public class Element {
return findChild(name, xmlns) != null;
}
- public List<Element> getChildren() {
+ public final List<Element> getChildren() {
return this.children;
}
public Element setChildren(List<Element> children) {
+ this.childNodes = new ArrayList(children);
this.children = children;
return this;
}
public final String getContent() {
- return content;
+ return this.childNodes.stream().map(Node::getContent).filter(c -> c != null).collect(Collectors.joining());
}
public Element setAttribute(String name, String value) {
@@ 170,7 195,7 @@ public class Element {
@NotNull
public String toString() {
final StringBuilder elementOutput = new StringBuilder();
- if ((content == null) && (children.size() == 0)) {
+ if (childNodes.size() == 0) {
Tag emptyTag = Tag.empty(name);
emptyTag.setAtttributes(this.attributes);
elementOutput.append(emptyTag.toString());
@@ 178,12 203,8 @@ public class Element {
Tag startTag = Tag.start(name);
startTag.setAtttributes(this.attributes);
elementOutput.append(startTag);
- if (content != null) {
- elementOutput.append(XmlHelper.encodeEntities(content));
- } else {
- for (Element child : children) {
- elementOutput.append(child.toString());
- }
+ for (Node child : childNodes) {
+ elementOutput.append(child.toString());
}
Tag endTag = Tag.end(name);
elementOutput.append(endTag);
@@ 197,6 218,7 @@ public class Element {
public void clearChildren() {
this.children.clear();
+ this.childNodes.clear();
}
public void setAttribute(String name, long value) {
M src/main/java/eu/siacs/conversations/xml/LocalizedContent.java => src/main/java/eu/siacs/conversations/xml/LocalizedContent.java +1 -1
@@ 23,7 23,7 @@ public class LocalizedContent {
public static LocalizedContent get(final Element element, String name) {
final HashMap<String, String> contents = new HashMap<>();
final String parentLanguage = element.getAttribute("xml:lang");
- for(Element child : element.children) {
+ for(Element child : element.getChildren()) {
if (name.equals(child.getName())) {
final String namespace = child.getNamespace();
final String childLanguage = child.getAttribute("xml:lang");
A src/main/java/eu/siacs/conversations/xml/Node.java => src/main/java/eu/siacs/conversations/xml/Node.java +5 -0
@@ 0,0 1,5 @@
+package eu.siacs.conversations.xml;
+
+public interface Node {
+ public String getContent();
+}
A src/main/java/eu/siacs/conversations/xml/TextNode.java => src/main/java/eu/siacs/conversations/xml/TextNode.java +20 -0
@@ 0,0 1,20 @@
+package eu.siacs.conversations.xml;
+
+import eu.siacs.conversations.utils.XmlHelper;
+
+public class TextNode implements Node {
+ protected String content;
+
+ public TextNode(final String content) {
+ if (content == null) throw new IllegalArgumentException("null TextNode is not allowed");
+ this.content = content;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public String toString() {
+ return XmlHelper.encodeEntities(content);
+ }
+}
M src/main/java/eu/siacs/conversations/xml/XmlReader.java => src/main/java/eu/siacs/conversations/xml/XmlReader.java +3 -8
@@ 94,15 94,10 @@ public class XmlReader implements Closeable {
if (nextTag == null) {
throw new IOException("interrupted mid tag");
}
- if (nextTag.isNo()) {
- element.setContent(nextTag.getName());
- nextTag = this.readTag();
- if (nextTag == null) {
- throw new IOException("interrupted mid tag");
- }
- }
while (!nextTag.isEnd(element.getName())) {
- if (!nextTag.isNo()) {
+ if (nextTag.isNo()) {
+ element.addChild(new TextNode(nextTag.getName()));
+ } else {
Element child = this.readElement(nextTag);
element.addChild(child);
}
M src/main/java/eu/siacs/conversations/xmpp/forms/Data.java => src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +2 -7
@@ 4,8 4,8 @@ import android.os.Bundle;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Iterator;
import java.util.List;
+import java.util.stream.Collectors;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
@@ 77,12 77,7 @@ public class Data extends Element {
}
private void removeUnnecessaryChildren() {
- for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) {
- Element element = iterator.next();
- if (!element.getName().equals("field") && !element.getName().equals("title")) {
- iterator.remove();
- }
- }
+ setChildren(getChildren().stream().filter(element -> element.getName().equals("field") || element.getName().equals("title")).collect(Collectors.toList()));
}
public static Data parse(Element element) {
M src/main/java/eu/siacs/conversations/xmpp/forms/Field.java => src/main/java/eu/siacs/conversations/xmpp/forms/Field.java +4 -13
@@ 2,8 2,8 @@ package eu.siacs.conversations.xmpp.forms;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Iterator;
import java.util.List;
+import java.util.stream.Collectors;
import eu.siacs.conversations.xml.Element;
@@ 23,24 23,15 @@ public class Field extends Element {
}
public void setValue(String value) {
- this.children.clear();
- this.addChild("value").setContent(value);
+ setChildren(List.of(new Element("value").setContent(value)));
}
public void setValues(Collection<String> values) {
- this.children.clear();
- for(String value : values) {
- this.addChild("value").setContent(value);
- }
+ setChildren(values.stream().map(val -> new Element("value").setContent(val)).collect(Collectors.toList()));
}
public void removeNonValueChildren() {
- for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) {
- Element element = iterator.next();
- if (!element.getName().equals("value")) {
- iterator.remove();
- }
- }
+ setChildren(getChildren().stream().filter(element -> element.getName().equals("value")).collect(Collectors.toList()));
}
public static Field parse(Element element) {
M src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java => src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java +1 -1
@@ 29,7 29,7 @@ public class Group extends Element {
public List<String> getIdentificationTags() {
final ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
- for (final Element child : this.children) {
+ for (final Element child : getChildren()) {
if ("content".equals(child.getName())) {
final String name = child.getAttribute("name");
if (name != null) {
M src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java => src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java +1 -1
@@ 15,7 15,7 @@ public class Propose extends Element {
public List<GenericDescription> getDescriptions() {
final ImmutableList.Builder<GenericDescription> builder = new ImmutableList.Builder<>();
- for (final Element child : this.children) {
+ for (final Element child : getChildren()) {
if ("description".equals(child.getName())) {
final String namespace = child.getNamespace();
if (FileTransferDescription.NAMESPACES.contains(namespace)) {
M src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java => src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +5 -19
@@ 66,7 66,7 @@ public class RtpDescription extends GenericDescription {
public List<Source> getSources() {
final ImmutableList.Builder<Source> builder = new ImmutableList.Builder<>();
- for (final Element child : this.children) {
+ for (final Element child : getChildren()) {
if ("source".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) {
builder.add(Source.upgrade(child));
}
@@ 76,7 76,7 @@ public class RtpDescription extends GenericDescription {
public List<SourceGroup> getSourceGroups() {
final ImmutableList.Builder<SourceGroup> builder = new ImmutableList.Builder<>();
- for (final Element child : this.children) {
+ for (final Element child : getChildren()) {
if ("ssrc-group".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) {
builder.add(SourceGroup.upgrade(child));
}
@@ 326,16 326,8 @@ public class RtpDescription extends GenericDescription {
return null;
}
- public void addChildren(final List<Element> children) {
- if (children != null) {
- this.children.addAll(children);
- }
- }
-
public void addParameters(List<Parameter> parameters) {
- if (parameters != null) {
- this.children.addAll(parameters);
- }
+ addChildren(parameters);
}
}
@@ 442,7 434,7 @@ public class RtpDescription extends GenericDescription {
public List<Parameter> getParameters() {
ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
- for (Element child : this.children) {
+ for (Element child : getChildren()) {
if ("parameter".equals(child.getName())) {
builder.add(Parameter.upgrade(child));
}
@@ 512,7 504,7 @@ public class RtpDescription extends GenericDescription {
public List<String> getSsrcs() {
ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
- for (Element child : this.children) {
+ for (Element child : getChildren()) {
if ("source".equals(child.getName())) {
final String ssrc = child.getAttribute("ssrc");
if (ssrc != null) {
@@ 610,10 602,4 @@ public class RtpDescription extends GenericDescription {
}
return rtpDescription;
}
-
- private void addChildren(List<Element> elements) {
- if (elements != null) {
- this.children.addAll(elements);
- }
- }
}
M src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java => src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +4 -6
@@ 22,15 22,13 @@ public class MessagePacket extends AbstractAcknowledgeableStanza {
}
public void setBody(String text) {
- this.children.remove(findChild("body"));
- Element body = new Element("body");
- body.setContent(text);
- this.children.add(0, body);
+ removeChild(findChild("body"));
+ prependChild(new Element("body").setContent(text));
}
public void setAxolotlMessage(Element axolotlMessage) {
- this.children.remove(findChild("body"));
- this.children.add(0, axolotlMessage);
+ removeChild(findChild("body"));
+ prependChild(axolotlMessage);
}
public void setType(int type) {