1package eu.siacs.conversations.xmpp.jingle;
2
3import java.io.IOException;
4import java.io.InputStream;
5import java.io.OutputStream;
6import java.security.MessageDigest;
7import java.security.NoSuchAlgorithmException;
8import java.util.Arrays;
9
10import android.util.Base64;
11
12import eu.siacs.conversations.entities.Account;
13import eu.siacs.conversations.entities.DownloadableFile;
14import eu.siacs.conversations.utils.CryptoHelper;
15import eu.siacs.conversations.xml.Element;
16import eu.siacs.conversations.xmpp.OnIqPacketReceived;
17import eu.siacs.conversations.xmpp.jid.Jid;
18import eu.siacs.conversations.xmpp.stanzas.IqPacket;
19
20public class JingleInbandTransport extends JingleTransport {
21
22 private Account account;
23 private Jid counterpart;
24 private int blockSize;
25 private int bufferSize;
26 private int seq = 0;
27 private String sessionId;
28
29 private boolean established = false;
30
31 private boolean connected = true;
32
33 private DownloadableFile file;
34 private JingleConnection connection;
35
36 private InputStream fileInputStream = null;
37 private OutputStream fileOutputStream;
38 private long remainingSize = 0;
39 private long fileSize = 0;
40 private MessageDigest digest;
41
42 private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
43
44 private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
45 @Override
46 public void onIqPacketReceived(Account account, IqPacket packet) {
47 if (connected && packet.getType() == IqPacket.TYPE_RESULT) {
48 sendNextBlock();
49 }
50 }
51 };
52
53 public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) {
54 this.connection = connection;
55 this.account = connection.getAccount();
56 this.counterpart = connection.getCounterPart();
57 this.blockSize = blocksize;
58 this.bufferSize = blocksize / 4;
59 this.sessionId = sid;
60 }
61
62 public void connect(final OnTransportConnected callback) {
63 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
64 iq.setTo(this.counterpart);
65 Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
66 open.setAttribute("sid", this.sessionId);
67 open.setAttribute("stanza", "iq");
68 open.setAttribute("block-size", Integer.toString(this.blockSize));
69 this.connected = true;
70 this.account.getXmppConnection().sendIqPacket(iq,
71 new OnIqPacketReceived() {
72
73 @Override
74 public void onIqPacketReceived(Account account,
75 IqPacket packet) {
76 if (packet.getType() == IqPacket.TYPE_ERROR) {
77 callback.failed();
78 } else {
79 callback.established();
80 }
81 }
82 });
83 }
84
85 @Override
86 public void receive(DownloadableFile file,
87 OnFileTransmissionStatusChanged callback) {
88 this.onFileTransmissionStatusChanged = callback;
89 this.file = file;
90 try {
91 this.digest = MessageDigest.getInstance("SHA-1");
92 digest.reset();
93 file.getParentFile().mkdirs();
94 file.createNewFile();
95 this.fileOutputStream = file.createOutputStream();
96 if (this.fileOutputStream == null) {
97 callback.onFileTransferAborted();
98 return;
99 }
100 this.remainingSize = this.fileSize = file.getExpectedSize();
101 } catch (final NoSuchAlgorithmException | IOException e) {
102 callback.onFileTransferAborted();
103 }
104 }
105
106 @Override
107 public void send(DownloadableFile file,
108 OnFileTransmissionStatusChanged callback) {
109 this.onFileTransmissionStatusChanged = callback;
110 this.file = file;
111 try {
112 this.remainingSize = this.file.getSize();
113 this.fileSize = this.remainingSize;
114 this.digest = MessageDigest.getInstance("SHA-1");
115 this.digest.reset();
116 fileInputStream = this.file.createInputStream();
117 if (fileInputStream == null) {
118 callback.onFileTransferAborted();
119 return;
120 }
121 if (this.connected) {
122 this.sendNextBlock();
123 }
124 } catch (NoSuchAlgorithmException e) {
125 callback.onFileTransferAborted();
126 }
127 }
128
129 @Override
130 public void disconnect() {
131 this.connected = false;
132 }
133
134 private void sendNextBlock() {
135 byte[] buffer = new byte[this.bufferSize];
136 try {
137 int count = fileInputStream.read(buffer);
138 if (count == -1) {
139 file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
140 fileInputStream.close();
141 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
142 } else {
143 this.remainingSize -= count;
144 this.digest.update(buffer);
145 String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP);
146 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
147 iq.setTo(this.counterpart);
148 Element data = iq.addChild("data",
149 "http://jabber.org/protocol/ibb");
150 data.setAttribute("seq", Integer.toString(this.seq));
151 data.setAttribute("block-size",
152 Integer.toString(this.blockSize));
153 data.setAttribute("sid", this.sessionId);
154 data.setContent(base64);
155 this.account.getXmppConnection().sendIqPacket(iq,
156 this.onAckReceived);
157 this.seq++;
158 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
159 }
160 } catch (IOException e) {
161 this.onFileTransmissionStatusChanged.onFileTransferAborted();
162 }
163 }
164
165 private void receiveNextBlock(String data) {
166 try {
167 byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
168 if (this.remainingSize < buffer.length) {
169 buffer = Arrays
170 .copyOfRange(buffer, 0, (int) this.remainingSize);
171 }
172 this.remainingSize -= buffer.length;
173
174
175 this.fileOutputStream.write(buffer);
176
177 this.digest.update(buffer);
178 if (this.remainingSize <= 0) {
179 file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
180 fileOutputStream.flush();
181 fileOutputStream.close();
182 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
183 } else {
184 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
185 }
186 } catch (IOException e) {
187 this.onFileTransmissionStatusChanged.onFileTransferAborted();
188 }
189 }
190
191 public void deliverPayload(IqPacket packet, Element payload) {
192 if (payload.getName().equals("open")) {
193 if (!established) {
194 established = true;
195 connected = true;
196 this.account.getXmppConnection().sendIqPacket(
197 packet.generateRespone(IqPacket.TYPE_RESULT), null);
198 } else {
199 this.account.getXmppConnection().sendIqPacket(
200 packet.generateRespone(IqPacket.TYPE_ERROR), null);
201 }
202 } else if (connected && payload.getName().equals("data")) {
203 this.receiveNextBlock(payload.getContent());
204 this.account.getXmppConnection().sendIqPacket(
205 packet.generateRespone(IqPacket.TYPE_RESULT), null);
206 } else {
207 // TODO some sort of exception
208 }
209 }
210}