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