create 'Description' object

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java                 |  10 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java         |  14 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java    | 173 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java           |   9 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java                 |  75 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java |  89 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java      |  20 
7 files changed, 237 insertions(+), 153 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java 🔗

@@ -20,14 +20,14 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.PhoneHelper;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.XmppConnection;
-import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
 
 public abstract class AbstractGenerator {
 	private final String[] FEATURES = {
-			"urn:xmpp:jingle:1",
-			Content.Version.FT_3.getNamespace(),
-			Content.Version.FT_4.getNamespace(),
-			Content.Version.FT_5.getNamespace(),
+			Namespace.JINGLE,
+			FileTransferDescription.Version.FT_3.getNamespace(),
+			FileTransferDescription.Version.FT_4.getNamespace(),
+			FileTransferDescription.Version.FT_5.getNamespace(),
 			Namespace.JINGLE_TRANSPORTS_S5B,
 			Namespace.JINGLE_TRANSPORTS_IBB,
 			Namespace.JINGLE_ENCRYPTED_TRANSPORT,

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java 🔗

@@ -18,6 +18,8 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
 import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 import rocks.xmpp.addr.Jid;
@@ -34,9 +36,17 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     public void deliverPacket(final Account account, final JinglePacket packet) {
         final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet);
         if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) { //TODO check that id doesn't exist yet
-            JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id);
-            connection.init(account, packet);
+            final Content content = packet.getJingleContent();
+            final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace();
+            final AbstractJingleConnection connection;
+            if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
+                connection = new JingleFileTransferConnection(this, id);
+            } else {
+                //TODO return feature-not-implemented
+                return;
+            }
             connections.put(id, connection);
+            connection.deliverPacket(packet);
         } else {
             final AbstractJingleConnection abstractJingleConnection = connections.get(id);
             if (abstractJingleConnection != null) {

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java 🔗

@@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle;
 import android.util.Base64;
 import android.util.Log;
 
+import com.google.common.base.Preconditions;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -36,6 +38,7 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
 import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
@@ -50,7 +53,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
     private static final int JINGLE_STATUS_FINISHED = 4;
     private static final int JINGLE_STATUS_FAILED = 99;
     private static final int JINGLE_STATUS_OFFERED = -1;
-    private Content.Version ftVersion = Content.Version.FT_3;
 
     private int ibbBlockSize = 8192;
 
@@ -63,7 +65,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
     private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
 
     private String transportId;
-    private Element fileOffer;
+    private FileTransferDescription description;
     private DownloadableFile file = null;
 
     private boolean proxyActivationFailed = false;
@@ -130,7 +132,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
                     id.account.getPgpDecryptionService().decrypt(message, true);
                 }
             } else {
-                if (ftVersion == Content.Version.FT_5) { //older Conversations will break when receiving a session-info
+                if (description.getVersion() == FileTransferDescription.Version.FT_5) { //older Conversations will break when receiving a session-info
                     sendHash();
                 }
                 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
@@ -236,7 +238,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
     void deliverPacket(final JinglePacket packet) {
         final JinglePacket.Action action = packet.getAction();
         //TODO switch case
-        if (action == JinglePacket.Action.SESSION_TERMINATE) {
+        if (action == JinglePacket.Action.SESSION_INITIATE) {
+            init(packet);
+        } else if (action == JinglePacket.Action.SESSION_TERMINATE) {
             Reason reason = packet.getReason();
             if (reason != null) {
                 if (reason.hasChild("cancel")) {
@@ -307,6 +311,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
     }
 
     public void init(final Message message) {
+        Preconditions.checkArgument(message.isFileOrImage());
         if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
             Conversation conversation = (Conversation) message.getConversation();
             conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, xmppAxolotlMessage -> {
@@ -321,13 +326,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         }
     }
 
-    private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
+    private void init(final Message message, final XmppAxolotlMessage xmppAxolotlMessage) {
         this.mXmppAxolotlMessage = xmppAxolotlMessage;
         this.contentCreator = Content.Creator.INITIATOR;
         this.contentName = JingleConnectionManager.nextRandomId();
         this.message = message;
         final List<String> remoteFeatures = getRemoteFeatures();
-        upgradeNamespace(remoteFeatures);
+        final FileTransferDescription.Version remoteVersion = getAvailableFileTransferVersion(remoteFeatures);
         this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? Transport.SOCKS : Transport.IBB;
         this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO);
         this.message.setTransferable(this);
@@ -335,6 +340,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         this.initiator = this.id.account.getJid();
         this.responder = this.id.counterPart;
         this.transportId = JingleConnectionManager.nextRandomId();
+        this.setupDescription(remoteVersion);
         if (this.initialTransport == Transport.IBB) {
             this.sendInitRequest();
         } else {
@@ -386,11 +392,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         }
     }
 
-    private void upgradeNamespace(List<String> remoteFeatures) {
-        if (remoteFeatures.contains(Content.Version.FT_5.getNamespace())) {
-            this.ftVersion = Content.Version.FT_5;
-        } else if (remoteFeatures.contains(Content.Version.FT_4.getNamespace())) {
-            this.ftVersion = Content.Version.FT_4;
+    private FileTransferDescription.Version getAvailableFileTransferVersion(List<String> remoteFeatures) {
+        if (remoteFeatures.contains(FileTransferDescription.Version.FT_5.getNamespace())) {
+            return FileTransferDescription.Version.FT_5;
+        } else if (remoteFeatures.contains(FileTransferDescription.Version.FT_4.getNamespace())) {
+            return FileTransferDescription.Version.FT_4;
+        } else {
+            return FileTransferDescription.Version.FT_3;
         }
     }
 
@@ -406,11 +414,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         }
     }
 
-    public void init(Account account, JinglePacket packet) { //should move to deliverPacket
+    private void init(JinglePacket packet) { //should move to deliverPacket
         this.mJingleStatus = JINGLE_STATUS_INITIATED;
-        Conversation conversation = this.xmppConnectionService
-                .findOrCreateConversation(account,
-                        packet.getFrom().asBareJid(), false, false);
+        final Conversation conversation = this.xmppConnectionService.findOrCreateConversation(id.account, id.counterPart.asBareJid(), false, false);
         this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
         this.message.setStatus(Message.STATUS_RECEIVED);
         this.mStatus = Transferable.STATUS_OFFER;
@@ -445,14 +451,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
                 return;
             }
         }
-        this.ftVersion = content.getVersion();
-        if (ftVersion == null) {
-            respondToIq(packet, false);
-            this.fail();
-            return;
-        }
-        this.fileOffer = content.getFileOffer(this.ftVersion);
 
+        this.description = (FileTransferDescription) content.getDescription();
+
+        final Element fileOffer = this.description.getFileOffer();
 
         if (fileOffer != null) {
             boolean remoteIsUsingJet = false;
@@ -536,71 +538,76 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         }
     }
 
+    private void setupDescription(final FileTransferDescription.Version version) {
+        this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
+        final FileTransferDescription description;
+        if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+            this.file.setKey(mXmppAxolotlMessage.getInnerKey());
+            this.file.setIv(mXmppAxolotlMessage.getIV());
+            //legacy OMEMO encrypted file transfer reported file size of the encrypted file
+            //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag)
+            this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16));
+            if (remoteSupportsOmemoJet) {
+                description = FileTransferDescription.of(this.file, version, null);
+            } else {
+                description = FileTransferDescription.of(this.file, version, this.mXmppAxolotlMessage);
+            }
+        } else {
+            this.file.setExpectedSize(file.getSize());
+            description = FileTransferDescription.of(this.file, version, null);
+        }
+        this.description = description;
+    }
+
     private void sendInitRequest() {
         final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE);
         final Content content = new Content(this.contentCreator, this.contentName);
-        if (message.isFileOrImage()) {
-            content.setTransportId(this.transportId);
-            this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
-            if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
-                this.file.setKey(mXmppAxolotlMessage.getInnerKey());
-                this.file.setIv(mXmppAxolotlMessage.getIV());
-                //legacy OMEMO encrypted file transfer reported file size of the encrypted file
-                //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag)
-                this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16));
-                final Element file = content.setFileOffer(this.file, false, this.ftVersion);
-                if (remoteSupportsOmemoJet) {
-                    Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET");
-                    final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
-                    security.setAttribute("name", this.contentName);
-                    security.setAttribute("cipher", JET_OMEMO_CIPHER);
-                    security.setAttribute("type", AxolotlService.PEP_PREFIX);
-                    security.addChild(mXmppAxolotlMessage.toElement());
-                    content.addChild(security);
+        content.setTransportId(this.transportId);
+        if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) {
+            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET");
+            final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
+            security.setAttribute("name", this.contentName);
+            security.setAttribute("cipher", JET_OMEMO_CIPHER);
+            security.setAttribute("type", AxolotlService.PEP_PREFIX);
+            security.addChild(mXmppAxolotlMessage.toElement());
+            content.addChild(security);
+        }
+        content.setDescription(this.description);
+        message.resetFileParams();
+        try {
+            this.mFileInputStream = new FileInputStream(file);
+        } catch (FileNotFoundException e) {
+            fail(e.getMessage());
+            return;
+        }
+        content.setTransportId(this.transportId);
+        if (this.initialTransport == Transport.IBB) {
+            content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize));
+            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer");
+        } else {
+            final List<Element> candidates = getCandidatesAsElements();
+            Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size()));
+            content.socks5transport().setChildren(candidates);
+        }
+        packet.setJingleContent(content);
+        this.sendJinglePacket(packet, (account, response) -> {
+            if (response.getType() == IqPacket.TYPE.RESULT) {
+                Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer");
+                if (mJingleStatus == JINGLE_STATUS_OFFERED) {
+                    mJingleStatus = JINGLE_STATUS_INITIATED;
+                    xmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
                 } else {
-                    file.addChild(mXmppAxolotlMessage.toElement());
+                    Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus);
                 }
             } else {
-                this.file.setExpectedSize(file.getSize());
-                content.setFileOffer(this.file, false, this.ftVersion);
-            }
-            message.resetFileParams();
-            try {
-                this.mFileInputStream = new FileInputStream(file);
-            } catch (FileNotFoundException e) {
-                fail(e.getMessage());
-                return;
-            }
-            content.setTransportId(this.transportId);
-            if (this.initialTransport == Transport.IBB) {
-                content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize));
-                Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer");
-            } else {
-                final List<Element> candidates = getCandidatesAsElements();
-                Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size()));
-                content.socks5transport().setChildren(candidates);
+                fail(IqParser.extractErrorMessage(response));
             }
-            packet.setJingleContent(content);
-            this.sendJinglePacket(packet, (account, response) -> {
-                if (response.getType() == IqPacket.TYPE.RESULT) {
-                    Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer");
-                    if (mJingleStatus == JINGLE_STATUS_OFFERED) {
-                        mJingleStatus = JINGLE_STATUS_INITIATED;
-                        xmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
-                    } else {
-                        Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus);
-                    }
-                } else {
-                    fail(IqParser.extractErrorMessage(response));
-                }
-            });
+        });
 
-        }
     }
 
     private void sendHash() {
-
-        final Element checksum = new Element("checksum", ftVersion.getNamespace());
+        final Element checksum = new Element("checksum", description.getVersion().getNamespace());
         checksum.setAttribute("creator", "initiator");
         checksum.setAttribute("name", "a-file-offer");
         Element hash = checksum.addChild("file").addChild("hash", "urn:xmpp:hashes:2");
@@ -637,7 +644,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         this.jingleConnectionManager.getPrimaryCandidate(this.id.account, initiating(), (success, candidate) -> {
             final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
             final Content content = new Content(contentCreator, contentName);
-            content.setFileOffer(fileOffer, ftVersion);
+            content.setDescription(this.description);
             content.setTransportId(transportId);
             if (success && candidate != null && !equalCandidateExists(candidate)) {
                 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
@@ -677,7 +684,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
         final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
         final Content content = new Content(contentCreator, contentName);
-        content.setFileOffer(fileOffer, ftVersion);
+        content.setDescription(this.description);
         content.setTransportId(transportId);
         content.ibbTransport().setAttribute("block-size", this.ibbBlockSize);
         packet.setJingleContent(content);
@@ -812,7 +819,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
             if (connection.needsActivation()) {
                 if (connection.getCandidate().isOurs()) {
                     final String sid;
-                    if (ftVersion == Content.Version.FT_3) {
+                    if (description.getVersion() == FileTransferDescription.Version.FT_3) {
                         Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy");
                         sid = id.sessionId;
                     } else {
@@ -1220,12 +1227,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
         return this.transportId;
     }
 
-    public Content.Version getFtVersion() {
-        return this.ftVersion;
-    }
-
-    public boolean hasTransportId(String sid) {
-        return sid.equals(this.transportId);
+    public FileTransferDescription.Version getFtVersion() {
+        return this.description.getVersion();
     }
 
     public JingleTransport getTransport() {

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java 🔗

@@ -24,6 +24,7 @@ import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.SocksSocketFactory;
 import eu.siacs.conversations.utils.WakeLockHelper;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
 
 public class JingleSocks5Transport extends JingleTransport {
 
@@ -52,7 +53,7 @@ public class JingleSocks5Transport extends JingleTransport {
         this.connection = jingleConnection;
         this.account = jingleConnection.getId().account;
         final StringBuilder destBuilder = new StringBuilder();
-        if (this.connection.getFtVersion() == Content.Version.FT_3) {
+        if (this.connection.getFtVersion() == FileTransferDescription.Version.FT_3) {
             Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination");
             destBuilder.append(this.connection.getId().sessionId);
         } else {
@@ -132,7 +133,7 @@ public class JingleSocks5Transport extends JingleTransport {
                 responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03};
                 success = true;
             } else {
-                Log.d(Config.LOGTAG,this.account.getJid().asBareJid()+": destination mismatch. received "+receivedDestination+" (expected "+this.destination+")");
+                Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": destination mismatch. received " + receivedDestination + " (expected " + this.destination + ")");
                 responseHeader = new byte[]{0x05, 0x04, 0x00, 0x03};
                 success = false;
             }
@@ -143,7 +144,7 @@ public class JingleSocks5Transport extends JingleTransport {
             outputStream.write(response.array());
             outputStream.flush();
             if (success) {
-                Log.d(Config.LOGTAG,this.account.getJid().asBareJid()+": successfully processed connection to candidate "+candidate.getHost()+":"+candidate.getPort());
+                Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": successfully processed connection to candidate " + candidate.getHost() + ":" + candidate.getPort());
                 socket.setSoTimeout(0);
                 this.socket = socket;
                 this.inputStream = inputStream;
@@ -216,7 +217,7 @@ public class JingleSocks5Transport extends JingleTransport {
                 }
             } catch (Exception e) {
                 final Account account = this.account;
-                Log.d(Config.LOGTAG, account.getJid().asBareJid()+": failed sending file after "+transmitted+"/"+file.getExpectedSize()+" ("+ socket.getInetAddress()+":"+socket.getPort()+")", e);
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed sending file after " + transmitted + "/" + file.getExpectedSize() + " (" + socket.getInetAddress() + ":" + socket.getPort() + ")", e);
                 callback.onFileTransferAborted();
             } finally {
                 FileBackend.close(fileInputStream);

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java 🔗

@@ -39,58 +39,35 @@ public class Content extends Element {
         return Creator.of(getAttribute("creator"));
     }
 
-    public Version getVersion() {
-        if (hasChild("description", Version.FT_3.namespace)) {
-            return Version.FT_3;
-        } else if (hasChild("description", Version.FT_4.namespace)) {
-            return Version.FT_4;
-        } else if (hasChild("description", Version.FT_5.namespace)) {
-            return Version.FT_5;
-        }
-        return null;
+    public Senders getSenders() {
+        return Senders.of(getAttribute("senders"));
     }
 
-    public Element setFileOffer(DownloadableFile actualFile, boolean otr, Version version) {
-        Element description = this.addChild("description", version.namespace);
-        Element file;
-        if (version == Version.FT_3) {
-            Element offer = description.addChild("offer");
-            file = offer.addChild("file");
-        } else {
-            file = description.addChild("file");
-        }
-        file.addChild("size").setContent(Long.toString(actualFile.getExpectedSize()));
-        if (otr) {
-            file.addChild("name").setContent(actualFile.getName() + ".otr");
-        } else {
-            file.addChild("name").setContent(actualFile.getName());
-        }
-        return file;
+    public void setSenders(Senders senders) {
+        this.setAttribute("senders", senders.toString());
     }
 
-    public Element getFileOffer(Version version) {
-        Element description = this.findChild("description", version.namespace);
+    public GenericDescription getDescription() {
+        final Element description = this.findChild("description");
         if (description == null) {
             return null;
         }
-        if (version == Version.FT_3) {
-            Element offer = description.findChild("offer");
-            if (offer == null) {
-                return null;
-            }
-            return offer.findChild("file");
+        final String xmlns = description.getNamespace();
+        if (FileTransferDescription.NAMESPACES.contains(xmlns)) {
+            return FileTransferDescription.upgrade(description);
         } else {
-            return description.findChild("file");
+            return GenericDescription.upgrade(description);
         }
     }
 
-    public void setFileOffer(Element fileOffer, Version version) {
-        Element description = this.addChild("description", version.namespace);
-        if (version == Version.FT_3) {
-            description.addChild("offer").addChild(fileOffer);
-        } else {
-            description.addChild(fileOffer);
-        }
+    public void setDescription(final GenericDescription description) {
+        Preconditions.checkNotNull(description);
+        this.addChild(description);
+    }
+
+    public String getDescriptionNamespace() {
+        final Element description = this.findChild("description");
+        return description == null ? null : description.getNamespace();
     }
 
     public String getTransportId() {
@@ -132,22 +109,6 @@ public class Content extends Element {
         return this.hasChild("transport", Namespace.JINGLE_TRANSPORTS_IBB);
     }
 
-    public enum Version {
-        FT_3("urn:xmpp:jingle:apps:file-transfer:3"),
-        FT_4("urn:xmpp:jingle:apps:file-transfer:4"),
-        FT_5("urn:xmpp:jingle:apps:file-transfer:5");
-
-        private final String namespace;
-
-        Version(String namespace) {
-            this.namespace = namespace;
-        }
-
-        public String getNamespace() {
-            return namespace;
-        }
-    }
-
     public enum Creator {
         INITIATOR, RESPONDER;
 

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java 🔗

@@ -0,0 +1,89 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import com.google.common.base.Preconditions;
+
+import java.util.Arrays;
+import java.util.List;
+
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.xml.Element;
+
+public class FileTransferDescription extends GenericDescription {
+
+    public static List<String> NAMESPACES = Arrays.asList(
+            Version.FT_3.namespace,
+            Version.FT_4.namespace,
+            Version.FT_5.namespace
+    );
+
+
+    private FileTransferDescription(String name, String namespace) {
+        super(name, namespace);
+    }
+
+    public Version getVersion() {
+        final String namespace = getNamespace();
+        if (namespace.equals(Version.FT_3.namespace)) {
+            return Version.FT_3;
+        } else if (namespace.equals(Version.FT_4.namespace)) {
+            return Version.FT_4;
+        } else if (namespace.equals(Version.FT_5.namespace)) {
+            return Version.FT_5;
+        } else {
+            throw new IllegalStateException("Unknown namespace");
+        }
+    }
+
+    public Element getFileOffer() {
+        final Version version = getVersion();
+        if (version == Version.FT_3) {
+            final Element offer = this.findChild("offer");
+            return offer == null ? null : offer.findChild("file");
+        } else {
+            return this.findChild("file");
+        }
+    }
+
+    public static FileTransferDescription of(DownloadableFile file, Version version, XmppAxolotlMessage axolotlMessage) {
+        final FileTransferDescription description = new FileTransferDescription("description", version.getNamespace());
+        final Element fileElement;
+        if (version == Version.FT_3) {
+            Element offer = description.addChild("offer");
+            fileElement = offer.addChild("file");
+        } else {
+            fileElement = description.addChild("file");
+        }
+        fileElement.addChild("size").setContent(Long.toString(file.getExpectedSize()));
+        fileElement.addChild("name").setContent(file.getName());
+        if (axolotlMessage != null) {
+            fileElement.addChild(axolotlMessage.toElement());
+        }
+        return description;
+    }
+
+    public static FileTransferDescription upgrade(final Element element) {
+        Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description");
+        Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace");
+        final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace());
+        description.setAttributes(element.getAttributes());
+        description.setChildren(element.getChildren());
+        return description;
+    }
+
+    public enum Version {
+        FT_3("urn:xmpp:jingle:apps:file-transfer:3"),
+        FT_4("urn:xmpp:jingle:apps:file-transfer:4"),
+        FT_5("urn:xmpp:jingle:apps:file-transfer:5");
+
+        private final String namespace;
+
+        Version(String namespace) {
+            this.namespace = namespace;
+        }
+
+        public String getNamespace() {
+            return namespace;
+        }
+    }
+}

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java 🔗

@@ -0,0 +1,20 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import com.google.common.base.Preconditions;
+
+import eu.siacs.conversations.xml.Element;
+
+public class GenericDescription extends Element {
+
+    protected GenericDescription(String name, final String namespace) {
+        super(name, namespace);
+    }
+
+    public static GenericDescription upgrade(final Element element) {
+        Preconditions.checkArgument("description".equals(element.getName()));
+        final GenericDescription description = new GenericDescription("description", element.getNamespace());
+        description.setAttributes(element.getAttributes());
+        description.setChildren(element.getChildren());
+        return description;
+    }
+}