JingleInBandTransport.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import android.util.Base64;
  4import android.util.Log;
  5
  6import com.google.common.base.Preconditions;
  7
  8import java.io.IOException;
  9import java.io.InputStream;
 10import java.io.OutputStream;
 11import java.security.MessageDigest;
 12import java.security.NoSuchAlgorithmException;
 13import java.util.Arrays;
 14
 15import eu.siacs.conversations.Config;
 16import eu.siacs.conversations.entities.Account;
 17import eu.siacs.conversations.entities.DownloadableFile;
 18import eu.siacs.conversations.persistance.FileBackend;
 19import eu.siacs.conversations.services.AbstractConnectionManager;
 20import eu.siacs.conversations.xml.Element;
 21import eu.siacs.conversations.xmpp.Jid;
 22import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 23import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 24
 25public class JingleInBandTransport extends JingleTransport {
 26
 27    private final Account account;
 28    private final Jid counterpart;
 29    private final int blockSize;
 30    private int seq = 0;
 31    private final String sessionId;
 32
 33    private boolean established = false;
 34
 35    private boolean connected = true;
 36
 37    private DownloadableFile file;
 38    private final JingleFileTransferConnection connection;
 39
 40    private InputStream fileInputStream = null;
 41    private InputStream innerInputStream = null;
 42    private OutputStream fileOutputStream = null;
 43    private long remainingSize = 0;
 44    private long fileSize = 0;
 45    private MessageDigest digest;
 46
 47    private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
 48
 49    private final OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
 50        @Override
 51        public void onIqPacketReceived(Account account, IqPacket packet) {
 52            if (!connected) {
 53                return;
 54            }
 55            if (packet.getType() == IqPacket.TYPE.RESULT) {
 56                if (remainingSize > 0) {
 57                    sendNextBlock();
 58                }
 59            } else if (packet.getType() == IqPacket.TYPE.ERROR) {
 60                onFileTransmissionStatusChanged.onFileTransferAborted();
 61            }
 62        }
 63    };
 64
 65    JingleInBandTransport(final JingleFileTransferConnection connection, final String sid, final int blockSize) {
 66        this.connection = connection;
 67        this.account = connection.getId().account;
 68        this.counterpart = connection.getId().with;
 69        this.blockSize = blockSize;
 70        this.sessionId = sid;
 71    }
 72
 73    private void sendClose() {
 74        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending ibb close");
 75        IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
 76        iq.setTo(this.counterpart);
 77        Element close = iq.addChild("close", "http://jabber.org/protocol/ibb");
 78        close.setAttribute("sid", this.sessionId);
 79        this.account.getXmppConnection().sendIqPacket(iq, null);
 80    }
 81
 82    public boolean matches(final Account account, final String sessionId) {
 83        return this.account == account && this.sessionId.equals(sessionId);
 84    }
 85
 86    public void connect(final OnTransportConnected callback) {
 87        IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
 88        iq.setTo(this.counterpart);
 89        Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
 90        open.setAttribute("sid", this.sessionId);
 91        open.setAttribute("stanza", "iq");
 92        open.setAttribute("block-size", Integer.toString(this.blockSize));
 93        this.connected = true;
 94        this.account.getXmppConnection().sendIqPacket(iq, (account, packet) -> {
 95            if (packet.getType() != IqPacket.TYPE.RESULT) {
 96                callback.failed();
 97            } else {
 98                callback.established();
 99            }
100        });
101    }
102
103    @Override
104    public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
105        this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback);
106        this.file = file;
107        try {
108            this.digest = MessageDigest.getInstance("SHA-1");
109            digest.reset();
110            this.fileOutputStream = connection.getFileOutputStream();
111            if (this.fileOutputStream == null) {
112                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not create output stream");
113                callback.onFileTransferAborted();
114                return;
115            }
116            this.remainingSize = this.fileSize = file.getExpectedSize();
117        } catch (final NoSuchAlgorithmException | IOException e) {
118            Log.d(Config.LOGTAG, account.getJid().asBareJid() + " " + e.getMessage());
119            callback.onFileTransferAborted();
120        }
121    }
122
123    @Override
124    public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
125        this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback);
126        this.file = file;
127        try {
128            this.remainingSize = this.file.getExpectedSize();
129            this.fileSize = this.remainingSize;
130            this.digest = MessageDigest.getInstance("SHA-1");
131            this.digest.reset();
132            fileInputStream = connection.getFileInputStream();
133            if (fileInputStream == null) {
134                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could no create input stream");
135                callback.onFileTransferAborted();
136                return;
137            }
138            innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
139            if (this.connected) {
140                this.sendNextBlock();
141            }
142        } catch (Exception e) {
143            callback.onFileTransferAborted();
144            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
145        }
146    }
147
148    @Override
149    public void disconnect() {
150        this.connected = false;
151        FileBackend.close(fileOutputStream);
152        FileBackend.close(fileInputStream);
153    }
154
155    private void sendNextBlock() {
156        byte[] buffer = new byte[this.blockSize];
157        try {
158            int count = innerInputStream.read(buffer);
159            if (count == -1) {
160                sendClose();
161                file.setSha1Sum(digest.digest());
162                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sendNextBlock() count was -1");
163                this.onFileTransmissionStatusChanged.onFileTransmitted(file);
164                fileInputStream.close();
165                return;
166            } else if (count != buffer.length) {
167                int rem = innerInputStream.read(buffer, count, buffer.length - count);
168                if (rem > 0) {
169                    count += rem;
170                }
171            }
172            this.remainingSize -= count;
173            this.digest.update(buffer, 0, count);
174            String base64 = Base64.encodeToString(buffer, 0, count, Base64.NO_WRAP);
175            IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
176            iq.setTo(this.counterpart);
177            Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
178            data.setAttribute("seq", Integer.toString(this.seq));
179            data.setAttribute("block-size", Integer.toString(this.blockSize));
180            data.setAttribute("sid", this.sessionId);
181            data.setContent(base64);
182            this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
183            this.account.getXmppConnection().r(); //don't fill up stanza queue too much
184            this.seq++;
185            connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
186            if (this.remainingSize <= 0) {
187                file.setSha1Sum(digest.digest());
188                this.onFileTransmissionStatusChanged.onFileTransmitted(file);
189                sendClose();
190                fileInputStream.close();
191            }
192        } catch (IOException e) {
193            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during sendNextBlock() " + e.getMessage());
194            FileBackend.close(fileInputStream);
195            this.onFileTransmissionStatusChanged.onFileTransferAborted();
196        }
197    }
198
199    private void receiveNextBlock(String data) {
200        try {
201            byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
202            if (this.remainingSize < buffer.length) {
203                buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
204            }
205            this.remainingSize -= buffer.length;
206            this.fileOutputStream.write(buffer);
207            this.digest.update(buffer);
208            if (this.remainingSize <= 0) {
209                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received last block. waiting for close");
210            } else {
211                connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
212            }
213        } catch (Exception e) {
214            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage(), e);
215            FileBackend.close(fileOutputStream);
216            this.onFileTransmissionStatusChanged.onFileTransferAborted();
217        }
218    }
219
220    private void done() {
221        try {
222            file.setSha1Sum(digest.digest());
223            fileOutputStream.flush();
224            fileOutputStream.close();
225            this.onFileTransmissionStatusChanged.onFileTransmitted(file);
226        } catch (Exception e) {
227            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
228            FileBackend.close(fileOutputStream);
229            this.onFileTransmissionStatusChanged.onFileTransferAborted();
230        }
231    }
232
233    void deliverPayload(IqPacket packet, Element payload) {
234        if (payload.getName().equals("open")) {
235            if (!established) {
236                established = true;
237                connected = true;
238                this.receiveNextBlock("");
239                this.account.getXmppConnection().sendIqPacket(
240                        packet.generateResponse(IqPacket.TYPE.RESULT), null);
241            } else {
242                this.account.getXmppConnection().sendIqPacket(
243                        packet.generateResponse(IqPacket.TYPE.ERROR), null);
244            }
245        } else if (connected && payload.getName().equals("data")) {
246            this.receiveNextBlock(payload.getContent());
247            this.account.getXmppConnection().sendIqPacket(
248                    packet.generateResponse(IqPacket.TYPE.RESULT), null);
249        } else if (connected && payload.getName().equals("close")) {
250            this.connected = false;
251            this.account.getXmppConnection().sendIqPacket(
252                    packet.generateResponse(IqPacket.TYPE.RESULT), null);
253            if (this.remainingSize <= 0) {
254                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close. done");
255                done();
256            } else {
257                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close with " + this.remainingSize + " remaining");
258                FileBackend.close(fileOutputStream);
259                this.onFileTransmissionStatusChanged.onFileTransferAborted();
260            }
261        } else {
262            this.account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
263        }
264    }
265}