diff --git a/src/cheogram/java/com/cheogram/android/BobTransfer.java b/src/cheogram/java/com/cheogram/android/BobTransfer.java index 1099b143081ea1f5dfa7c6a58e1c3d5fbe4ed60a..cbe30a302674891129c61620f970d0cb215d2625 100644 --- a/src/cheogram/java/com/cheogram/android/BobTransfer.java +++ b/src/cheogram/java/com/cheogram/android/BobTransfer.java @@ -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); + } } } diff --git a/src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java b/src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java new file mode 100644 index 0000000000000000000000000000000000000000..17e7d9db6129e4268fdce2c2ca5fb667e57098db --- /dev/null +++ b/src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java @@ -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); +} diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 42232c560f784d1b3cc13c9c325586eee82613ae..6da9283f982613af3a9673cc3a8ab76d9c4fdbb3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -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))) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 8367f7cba0da69838484bd194b9f5ba20f7417a3..05537efd27ac3855b425d2130ad6961558855288 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -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 diff --git a/src/main/java/eu/siacs/conversations/entities/Conversational.java b/src/main/java/eu/siacs/conversations/entities/Conversational.java index 58af42213c570a95c54494a721cc64e582fc25dd..9d5aa8e2b91d4103f357a7fb769720c9157d5290 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversational.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversational.java @@ -45,4 +45,6 @@ public interface Conversational { int getMode(); String getUuid(); + + boolean canInferPresence(); } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index db7229afaa579387d9533e73f8c25ad7587fbdca..5d84e743416e1a55d49c98081d1bce3880a1776d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -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 getCommands() { if (this.payloads == null) return null; diff --git a/src/main/java/eu/siacs/conversations/entities/StubConversation.java b/src/main/java/eu/siacs/conversations/entities/StubConversation.java index 79f4e80b4a65a60b7dc6f8881169d7955435036c..42f98e740233cc1be7f28e0a1dba44c6e4c22cc0 100644 --- a/src/main/java/eu/siacs/conversations/entities/StubConversation.java +++ b/src/main/java/eu/siacs/conversations/entities/StubConversation.java @@ -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(); + } } diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 706b50043a4b1dc2e39b420c496b7ec1af2c1865..3d1440384903f9b26d6de5e9349f0103d778d2a3 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -112,6 +112,7 @@ public abstract class AbstractGenerator { public List getFeatures(Account account) { final XmppConnection connection = account.getXmppConnection(); final ArrayList features = new ArrayList<>(Arrays.asList(FEATURES)); + features.add("http://jabber.org/protocol/xhtml-im"); if (mXmppConnectionService.confirmMessages()) { features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 0945e31a059233e79892b56963c5d75b922f5574..d6bd5ef3e91ae550218fa0e32d192fb10241bcb4 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -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) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index bf69e87613035244c94d62fdf3c827fa7690d924..4ab3b02d3c0824432fe027eac3848916789ff6a4 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index c2edaa3a15da9b4155e162762c37283e99f8402d..4a5f4d3aed5bf9339aba259c85802bd8fa6c2c12 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -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 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(); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index f8ca42db270c5916f52bcdd59c56c8a2eec4449d..3f4838eb5d6058beee104a08bdff8df9fbd8b293 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -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); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 6359e363c5b7300bcf4180210e9b834a7bec2a97..73bf6a556930429e866181d475fda3f68aad6483 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -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) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index f56c2a8e66c588b914d5cfbf8341be011dc635a6..7b57c0dc27c529f72c096e79d41bf9f3d86e840c 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -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 { 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 { protected TextView encryption; protected ListView commands_list; } + + class ThumbnailTask extends AsyncTask { + @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(); + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index c2d47ebe3c8ddbbe0092eaf285b56fa8676e3bdd..410a43838e80781838691618a332a5e06214e8d1 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 578bb2c89af778fcdce6318832222aedc941f2dc..7692138be38c01d2268ee43e7626cdae00719e86 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -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); } diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 4d53a17b723f3b179bf8446d82829b7ebc0b3348..d70e45ef6093e330bf8d352eb8cd562ba194c102 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -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 attributes = new Hashtable<>(); - private String content; - protected List children = new ArrayList<>(); + private List children = new ArrayList<>(); + private List 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 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 getChildren() { + public final List getChildren() { return this.children; } public Element setChildren(List 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) { diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java index fac5099a70f8efed7c8c0b73bbefb6ffbf5d4638..6fa71e9625e95891a9c868f94b13fff82b5c781e 100644 --- a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java +++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java @@ -23,7 +23,7 @@ public class LocalizedContent { public static LocalizedContent get(final Element element, String name) { final HashMap 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"); diff --git a/src/main/java/eu/siacs/conversations/xml/Node.java b/src/main/java/eu/siacs/conversations/xml/Node.java new file mode 100644 index 0000000000000000000000000000000000000000..dbe18d252ddc981f9b0d6c3e41d6f97fc211c8b0 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xml/Node.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.xml; + +public interface Node { + public String getContent(); +} diff --git a/src/main/java/eu/siacs/conversations/xml/TextNode.java b/src/main/java/eu/siacs/conversations/xml/TextNode.java new file mode 100644 index 0000000000000000000000000000000000000000..edbc1d312a6e282470d56f2ba279cd054b9ee52c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xml/TextNode.java @@ -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); + } +} diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index 240b92b7ae9ae11ab7cbbdfbca51403f8231d356..c775fd34c67804e00f21dbedbd26109dec3f9773 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -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); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java index e3bd9eb7481229914bd0fdc144373036420b7a53..16578967586c3f98a95074274b7950c48d6b8388 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java @@ -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 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) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java index d6ac14707b12dfff17b3937a51546790acf2a9e9..6e97b552c70c2070434cf3bfd170343e62052a5c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java @@ -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 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 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) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java index eb5c32252bf181d5037a02cbe1aaebf7ee8cd4f9..2f7873a860fc2d23dcabc75251578c333fc27b01 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java @@ -29,7 +29,7 @@ public class Group extends Element { public List getIdentificationTags() { final ImmutableList.Builder 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) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java index da3a93da301dde0d4b0dc19f3f972629d3d7af8e..f72162be8e5896e22184ee41f699c1caafa0e13a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java @@ -15,7 +15,7 @@ public class Propose extends Element { public List getDescriptions() { final ImmutableList.Builder 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)) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 650c26bef07af1eb1876641c4c4eb6c95cef3784..b1cb59a34d630230ffbcf0dcfd5b20979b971ac2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -66,7 +66,7 @@ public class RtpDescription extends GenericDescription { public List getSources() { final ImmutableList.Builder 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 getSourceGroups() { final ImmutableList.Builder 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 children) { - if (children != null) { - this.children.addAll(children); - } - } - public void addParameters(List parameters) { - if (parameters != null) { - this.children.addAll(parameters); - } + addChildren(parameters); } } @@ -442,7 +434,7 @@ public class RtpDescription extends GenericDescription { public List getParameters() { ImmutableList.Builder 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 getSsrcs() { ImmutableList.Builder 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 elements) { - if (elements != null) { - this.children.addAll(elements); - } - } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index 86068bf774efe9ed255c1def48812d0286ec8827..bac76adfc7f419c4f5bf10c953d43743772888be 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -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) {