JingleConnection.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import java.util.ArrayList;
  4import java.util.HashMap;
  5import java.util.Iterator;
  6import java.util.List;
  7import java.util.Map.Entry;
  8
  9import android.util.Log;
 10
 11import eu.siacs.conversations.entities.Account;
 12import eu.siacs.conversations.entities.Conversation;
 13import eu.siacs.conversations.entities.Message;
 14import eu.siacs.conversations.services.XmppConnectionService;
 15import eu.siacs.conversations.xml.Element;
 16import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 17import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 18import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 19import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 20import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 21
 22public class JingleConnection {
 23
 24	private JingleConnectionManager mJingleConnectionManager;
 25	private XmppConnectionService mXmppConnectionService;
 26	
 27	public static final int STATUS_INITIATED = 0;
 28	public static final int STATUS_ACCEPTED = 1;
 29	public static final int STATUS_TERMINATED = 2;
 30	public static final int STATUS_CANCELED = 3;
 31	public static final int STATUS_FINISHED = 4;
 32	public static final int STATUS_TRANSMITTING = 5;
 33	public static final int STATUS_FAILED = 99;
 34	
 35	private int status = -1;
 36	private Message message;
 37	private String sessionId;
 38	private Account account;
 39	private String initiator;
 40	private String responder;
 41	private List<Element> candidates = new ArrayList<Element>();
 42	private List<String> candidatesUsedByCounterpart = new ArrayList<String>();
 43	private HashMap<String, SocksConnection> connections = new HashMap<String, SocksConnection>();
 44	private Content content = new Content();
 45	private JingleFile file = null;
 46	
 47	private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
 48		
 49		@Override
 50		public void onIqPacketReceived(Account account, IqPacket packet) {
 51			if (packet.getType() == IqPacket.TYPE_ERROR) {
 52				mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
 53				status = STATUS_FAILED;
 54			}
 55		}
 56	};
 57	
 58	public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
 59		this.mJingleConnectionManager = mJingleConnectionManager;
 60		this.mXmppConnectionService = mJingleConnectionManager.getXmppConnectionService();
 61	}
 62	
 63	public String getSessionId() {
 64		return this.sessionId;
 65	}
 66	
 67	public String getAccountJid() {
 68		return this.account.getFullJid();
 69	}
 70	
 71	public String getCounterPart() {
 72		return this.message.getCounterpart();
 73	}
 74	
 75	public void deliverPacket(JinglePacket packet) {
 76		
 77		if (packet.isAction("session-terminate")) {
 78			Reason reason = packet.getReason();
 79			if (reason!=null) {
 80				if (reason.hasChild("cancel")) {
 81					this.cancel();
 82				} else if (reason.hasChild("success")) {
 83					this.finish();
 84				}
 85			} else {
 86				Log.d("xmppService","remote terminated for no reason");
 87				this.cancel();
 88			}
 89			} else if (packet.isAction("session-accept")) {
 90			accept(packet);
 91		} else if (packet.isAction("transport-info")) {
 92			transportInfo(packet);
 93		} else {
 94			Log.d("xmppService","packet arrived in connection. action was "+packet.getAction());
 95		}
 96	}
 97	
 98	public void init(Message message) {
 99		this.message = message;
100		this.account = message.getConversation().getAccount();
101		this.initiator = this.account.getFullJid();
102		this.responder = this.message.getCounterpart();
103		this.sessionId = this.mJingleConnectionManager.nextRandomId();
104		if (this.candidates.size() > 0) {
105			this.sendInitRequest();
106		} else {
107			this.mJingleConnectionManager.getPrimaryCandidate(account, new OnPrimaryCandidateFound() {
108				
109				@Override
110				public void onPrimaryCandidateFound(boolean success, Element candidate) {
111					if (success) {
112						mergeCandidate(candidate);
113					}
114					sendInitRequest();
115				}
116			});
117		}
118		
119	}
120	
121	public void init(Account account, JinglePacket packet) {
122		this.status = STATUS_INITIATED;
123		Conversation conversation = this.mXmppConnectionService.findOrCreateConversation(account, packet.getFrom().split("/")[0], false);
124		this.message = new Message(conversation, "receiving image file", Message.ENCRYPTION_NONE);
125		this.message.setType(Message.TYPE_IMAGE);
126		this.message.setStatus(Message.STATUS_RECIEVING);
127		String[] fromParts = packet.getFrom().split("/");
128		this.message.setPresence(fromParts[1]);
129		this.account = account;
130		this.initiator = packet.getFrom();
131		this.responder = this.account.getFullJid();
132		this.sessionId = packet.getSessionId();
133		this.content = packet.getJingleContent();
134		this.mergeCandidates(this.content.getCanditates());
135		Element fileOffer = packet.getJingleContent().getFileOffer();
136		if (fileOffer!=null) {
137			this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
138			Element fileSize = fileOffer.findChild("size");
139			Element fileName = fileOffer.findChild("name");
140			this.file.setExpectedSize(Long.parseLong(fileSize.getContent()));
141			conversation.getMessages().add(message);
142			this.mXmppConnectionService.databaseBackend.createMessage(message);
143			if (this.mXmppConnectionService.convChangedListener!=null) {
144				this.mXmppConnectionService.convChangedListener.onConversationListChanged();
145			}
146			if (this.file.getExpectedSize()>=this.mJingleConnectionManager.getAutoAcceptFileSize()) {
147				Log.d("xmppService","auto accepting file from "+packet.getFrom());
148				this.sendAccept();
149			} else {
150				Log.d("xmppService","not auto accepting new file offer with size: "+this.file.getExpectedSize()+" allowed size:"+this.mJingleConnectionManager.getAutoAcceptFileSize());
151			}
152		} else {
153			Log.d("xmppService","no file offer was attached. aborting");
154		}
155	}
156	
157	private void sendInitRequest() {
158		JinglePacket packet = this.bootstrapPacket("session-initiate");
159		this.content = new Content();
160		if (message.getType() == Message.TYPE_IMAGE) {
161			content.setAttribute("creator", "initiator");
162			content.setAttribute("name", "a-file-offer");
163			this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
164			content.setFileOffer(this.file);
165			content.setCandidates(this.mJingleConnectionManager.nextRandomId(),this.candidates);
166			packet.setContent(content);
167			Log.d("xmppService",packet.toString());
168			account.getXmppConnection().sendIqPacket(packet, this.responseListener);
169			this.status = STATUS_INITIATED;
170		}
171	}
172	
173	private void sendAccept() {
174		this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
175			
176			@Override
177			public void onPrimaryCandidateFound(boolean success, Element candidate) {
178				if (success) {
179					if (!equalCandidateExists(candidate)) {
180						mergeCandidate(candidate);
181						content.addCandidate(candidate);
182					}
183				}
184				JinglePacket packet = bootstrapPacket("session-accept");
185				packet.setContent(content);
186				account.getXmppConnection().sendIqPacket(packet, new OnIqPacketReceived() {
187					
188					@Override
189					public void onIqPacketReceived(Account account, IqPacket packet) {
190						if (packet.getType() != IqPacket.TYPE_ERROR) {
191							status = STATUS_ACCEPTED;
192							connectNextCandidate();
193						}
194					}
195				});
196			}
197		});
198		
199	}
200	
201	private JinglePacket bootstrapPacket(String action) {
202		JinglePacket packet = new JinglePacket();
203		packet.setAction(action);
204		packet.setFrom(account.getFullJid());
205		packet.setTo(this.message.getCounterpart()); //fixme, not right in all cases;
206		packet.setSessionId(this.sessionId);
207		packet.setInitiator(this.initiator);
208		return packet;
209	}
210	
211	private void accept(JinglePacket packet) {
212		Log.d("xmppService","session-accept: "+packet.toString());
213		Content content = packet.getJingleContent();
214		mergeCandidates(content.getCanditates());
215		this.status = STATUS_ACCEPTED;
216		this.connectNextCandidate();
217		IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
218		account.getXmppConnection().sendIqPacket(response, null);
219	}
220
221	private void transportInfo(JinglePacket packet) {
222		Content content = packet.getJingleContent();
223		String cid = content.getUsedCandidate();
224		IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
225		if (cid!=null) {
226			Log.d("xmppService","candidate used by counterpart:"+cid);
227			this.candidatesUsedByCounterpart.add(cid);
228			if (this.connections.containsKey(cid)) {
229				SocksConnection connection = this.connections.get(cid);
230				if (connection.isEstablished()) {
231					if (status==STATUS_ACCEPTED) {
232						this.connect(connection);
233					} else {
234						Log.d("xmppService","ignoring canditate used because we are already transmitting");
235					}
236				} else {
237					Log.d("xmppService","not yet connected. check when callback comes back");
238				}
239			} else {
240				Log.d("xmppService","candidate not yet in list of connections");
241			}
242		}
243		account.getXmppConnection().sendIqPacket(response, null);
244	}
245
246	private void connect(final SocksConnection connection) {
247		this.status = STATUS_TRANSMITTING;
248		final OnFileTransmitted callback = new OnFileTransmitted() {
249			
250			@Override
251			public void onFileTransmitted(JingleFile file) {
252				if (responder.equals(account.getFullJid())) {
253					sendSuccess();
254					mXmppConnectionService.markMessage(message, Message.STATUS_SEND);
255				}
256				Log.d("xmppService","sucessfully transmitted file. sha1:"+file.getSha1Sum());
257			}
258		};
259		if ((connection.isProxy()&&(connection.getCid().equals(mJingleConnectionManager.getPrimaryCandidateId(account))))) {
260			Log.d("xmppService","candidate "+connection.getCid()+" was our proxy and needs activation");
261			IqPacket activation = new IqPacket(IqPacket.TYPE_SET);
262			activation.setTo(connection.getJid());
263			activation.query("http://jabber.org/protocol/bytestreams").setAttribute("sid", this.getSessionId());
264			activation.query().addChild("activate").setContent(this.getCounterPart());
265			this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() {
266				
267				@Override
268				public void onIqPacketReceived(Account account, IqPacket packet) {
269					Log.d("xmppService","activation result: "+packet.toString());
270					if (initiator.equals(account.getFullJid())) {
271						Log.d("xmppService","we were initiating. sending file");
272						connection.send(file,callback);
273					} else {
274						connection.receive(file,callback);
275						Log.d("xmppService","we were responding. receiving file");
276					}
277				}
278			});
279		} else {
280			if (initiator.equals(account.getFullJid())) {
281				Log.d("xmppService","we were initiating. sending file");
282				connection.send(file,callback);
283			} else {
284				Log.d("xmppService","we were responding. receiving file");
285				connection.receive(file,callback);
286			}
287		}
288	}
289	
290	private void sendSuccess() {
291		JinglePacket packet = bootstrapPacket("session-terminate");
292		Reason reason = new Reason();
293		reason.addChild("success");
294		packet.setReason(reason);
295		Log.d("xmppService","sending success. "+packet.toString());
296		this.account.getXmppConnection().sendIqPacket(packet, responseListener);
297		this.disconnect();
298		this.status = STATUS_FINISHED;
299		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_RECIEVED);
300	}
301	
302	private void finish() {
303		this.status = STATUS_FINISHED;
304		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND);
305		this.disconnect();
306	}
307	
308	public void cancel() {
309		this.disconnect();
310		this.status = STATUS_CANCELED;
311		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_REJECTED);
312	}
313	
314	private void connectNextCandidate() {
315		for(Element candidate : this.candidates) {
316			String cid = candidate.getAttribute("cid");
317			if (!connections.containsKey(cid)) {
318				this.connectWithCandidate(candidate);
319				break;
320			}
321		}
322	}
323	
324	private void connectWithCandidate(Element candidate) {
325		boolean initating = candidate.getAttribute("cid").equals(mJingleConnectionManager.getPrimaryCandidateId(account));
326		final SocksConnection socksConnection = new SocksConnection(this,candidate,initating);
327		connections.put(socksConnection.getCid(), socksConnection);
328		socksConnection.connect(new OnSocksConnection() {
329			
330			@Override
331			public void failed() {
332				connectNextCandidate();
333			}
334			
335			@Override
336			public void established() {
337				if (candidatesUsedByCounterpart.contains(socksConnection.getCid())) {
338					if (status==STATUS_ACCEPTED) {
339						connect(socksConnection);
340					} else {
341						Log.d("xmppService","ignoring cuz already transmitting");
342					}
343				} else {
344					sendCandidateUsed(socksConnection.getCid());
345				}
346			}
347		});
348	}
349
350	private void disconnect() {
351		Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator();
352	    while (it.hasNext()) {
353	        Entry<String, SocksConnection> pairs = it.next();
354	        pairs.getValue().disconnect();
355	        it.remove();
356	    }
357	}
358	
359	private void sendCandidateUsed(final String cid) {
360		JinglePacket packet = bootstrapPacket("transport-info");
361		Content content = new Content();
362		content.setUsedCandidate(this.content.getTransportId(), cid);
363		packet.setContent(content);
364		Log.d("xmppService","send using candidate: "+cid);
365		this.account.getXmppConnection().sendIqPacket(packet, new OnIqPacketReceived() {
366			
367			@Override
368			public void onIqPacketReceived(Account account, IqPacket packet) {
369				Log.d("xmppService","got ack for our candidate used");
370				if (status==STATUS_ACCEPTED) {
371					connect(connections.get(cid));
372				} else {
373					Log.d("xmppService","ignoring cuz already transmitting");
374				}
375			}
376		});
377	}
378
379	public String getInitiator() {
380		return this.initiator;
381	}
382	
383	public String getResponder() {
384		return this.responder;
385	}
386	
387	public int getStatus() {
388		return this.status;
389	}
390	
391	private boolean equalCandidateExists(Element candidate) {
392		for(Element c : this.candidates) {
393			if (c.getAttribute("host").equals(candidate.getAttribute("host"))&&(c.getAttribute("port").equals(candidate.getAttribute("port")))) {
394				return true;
395			}
396		}
397		return false;
398	}
399	
400	private void mergeCandidate(Element candidate) {
401		for(Element c : this.candidates) {
402			if (c.getAttribute("cid").equals(candidate.getAttribute("cid"))) {
403				return;
404			}
405		}
406		this.candidates.add(candidate);
407	}
408	
409	private void mergeCandidates(List<Element> candidates) {
410		for(Element c : candidates) {
411			mergeCandidate(c);
412		}
413	}
414	
415	private Element getCandidate(String cid) {
416		for(Element c : this.candidates) {
417			if (c.getAttribute("cid").equals(cid)) {
418				return c;
419			}
420		}
421		return null;
422	}
423}