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<JingleCandidate> candidates = new ArrayList<JingleCandidate>();
 42	private HashMap<String, SocksConnection> connections = new HashMap<String, SocksConnection>();
 43	
 44	private String transportId;
 45	private Element fileOffer;
 46	private JingleFile file = null;
 47	
 48	private boolean receivedCandidateError = false;
 49	
 50	private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
 51		
 52		@Override
 53		public void onIqPacketReceived(Account account, IqPacket packet) {
 54			if (packet.getType() == IqPacket.TYPE_ERROR) {
 55				mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
 56				status = STATUS_FAILED;
 57			}
 58		}
 59	};
 60	
 61	public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
 62		this.mJingleConnectionManager = mJingleConnectionManager;
 63		this.mXmppConnectionService = mJingleConnectionManager.getXmppConnectionService();
 64	}
 65	
 66	public String getSessionId() {
 67		return this.sessionId;
 68	}
 69	
 70	public String getAccountJid() {
 71		return this.account.getFullJid();
 72	}
 73	
 74	public String getCounterPart() {
 75		return this.message.getCounterpart();
 76	}
 77	
 78	public void deliverPacket(JinglePacket packet) {
 79		
 80		if (packet.isAction("session-terminate")) {
 81			Reason reason = packet.getReason();
 82			if (reason!=null) {
 83				if (reason.hasChild("cancel")) {
 84					this.cancel();
 85				} else if (reason.hasChild("success")) {
 86					this.finish();
 87				}
 88			} else {
 89				Log.d("xmppService","remote terminated for no reason");
 90				this.cancel();
 91			}
 92			} else if (packet.isAction("session-accept")) {
 93			accept(packet);
 94		} else if (packet.isAction("transport-info")) {
 95			transportInfo(packet);
 96		} else {
 97			Log.d("xmppService","packet arrived in connection. action was "+packet.getAction());
 98		}
 99	}
100	
101	public void init(Message message) {
102		this.message = message;
103		this.account = message.getConversation().getAccount();
104		this.initiator = this.account.getFullJid();
105		this.responder = this.message.getCounterpart();
106		this.sessionId = this.mJingleConnectionManager.nextRandomId();
107		if (this.candidates.size() > 0) {
108			this.sendInitRequest();
109		} else {
110			this.mJingleConnectionManager.getPrimaryCandidate(account, new OnPrimaryCandidateFound() {
111				
112				@Override
113				public void onPrimaryCandidateFound(boolean success, JingleCandidate candidate) {
114					if (success) {
115						mergeCandidate(candidate);
116					}
117					openOurCandidates();
118					sendInitRequest();
119				}
120			});
121		}
122		
123	}
124	
125	public void init(Account account, JinglePacket packet) {
126		this.status = STATUS_INITIATED;
127		Conversation conversation = this.mXmppConnectionService.findOrCreateConversation(account, packet.getFrom().split("/")[0], false);
128		this.message = new Message(conversation, "receiving image file", Message.ENCRYPTION_NONE);
129		this.message.setType(Message.TYPE_IMAGE);
130		this.message.setStatus(Message.STATUS_RECIEVING);
131		String[] fromParts = packet.getFrom().split("/");
132		this.message.setPresence(fromParts[1]);
133		this.account = account;
134		this.initiator = packet.getFrom();
135		this.responder = this.account.getFullJid();
136		this.sessionId = packet.getSessionId();
137		Content content = packet.getJingleContent();
138		this.transportId = content.getTransportId();
139		this.mergeCandidates(JingleCandidate.parse(content.getCanditates()));
140		this.fileOffer = packet.getJingleContent().getFileOffer();
141		if (fileOffer!=null) {
142			this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
143			Element fileSize = fileOffer.findChild("size");
144			Element fileName = fileOffer.findChild("name");
145			this.file.setExpectedSize(Long.parseLong(fileSize.getContent()));
146			conversation.getMessages().add(message);
147			this.mXmppConnectionService.databaseBackend.createMessage(message);
148			if (this.mXmppConnectionService.convChangedListener!=null) {
149				this.mXmppConnectionService.convChangedListener.onConversationListChanged();
150			}
151			if (this.file.getExpectedSize()>=this.mJingleConnectionManager.getAutoAcceptFileSize()) {
152				Log.d("xmppService","auto accepting file from "+packet.getFrom());
153				this.sendAccept();
154			} else {
155				Log.d("xmppService","not auto accepting new file offer with size: "+this.file.getExpectedSize()+" allowed size:"+this.mJingleConnectionManager.getAutoAcceptFileSize());
156			}
157		} else {
158			Log.d("xmppService","no file offer was attached. aborting");
159		}
160	}
161	
162	private void sendInitRequest() {
163		JinglePacket packet = this.bootstrapPacket("session-initiate");
164		Content content = new Content();
165		if (message.getType() == Message.TYPE_IMAGE) {
166			content.setAttribute("creator", "initiator");
167			content.setAttribute("name", "a-file-offer");
168			this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
169			content.setFileOffer(this.file);
170			this.transportId = this.mJingleConnectionManager.nextRandomId();
171			content.setCandidates(this.transportId,getCandidatesAsElements());
172			packet.setContent(content);
173			Log.d("xmppService",packet.toString());
174			account.getXmppConnection().sendIqPacket(packet, this.responseListener);
175			this.status = STATUS_INITIATED;
176		}
177	}
178	
179	private List<Element> getCandidatesAsElements() {
180		List<Element> elements = new ArrayList<Element>();
181		for(JingleCandidate c : this.candidates) {
182			elements.add(c.toElement());
183		}
184		return elements;
185	}
186	
187	private void sendAccept() {
188		this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
189			
190			@Override
191			public void onPrimaryCandidateFound(boolean success, JingleCandidate candidate) {
192				Content content = new Content();
193				content.setFileOffer(fileOffer);
194				if (success) {
195					if (!equalCandidateExists(candidate)) {
196						mergeCandidate(candidate);
197					}
198				}
199				openOurCandidates();
200				content.setCandidates(transportId, getCandidatesAsElements());
201				JinglePacket packet = bootstrapPacket("session-accept");
202				packet.setContent(content);
203				account.getXmppConnection().sendIqPacket(packet, new OnIqPacketReceived() {
204					
205					@Override
206					public void onIqPacketReceived(Account account, IqPacket packet) {
207						if (packet.getType() != IqPacket.TYPE_ERROR) {
208							status = STATUS_ACCEPTED;
209							connectNextCandidate();
210						}
211					}
212				});
213			}
214		});
215		
216	}
217	
218	private JinglePacket bootstrapPacket(String action) {
219		JinglePacket packet = new JinglePacket();
220		packet.setAction(action);
221		packet.setFrom(account.getFullJid());
222		packet.setTo(this.message.getCounterpart()); //fixme, not right in all cases;
223		packet.setSessionId(this.sessionId);
224		packet.setInitiator(this.initiator);
225		return packet;
226	}
227	
228	private void accept(JinglePacket packet) {
229		Log.d("xmppService","session-accept: "+packet.toString());
230		Content content = packet.getJingleContent();
231		mergeCandidates(JingleCandidate.parse(content.getCanditates()));
232		this.status = STATUS_ACCEPTED;
233		this.connectNextCandidate();
234		IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
235		account.getXmppConnection().sendIqPacket(response, null);
236	}
237
238	private void transportInfo(JinglePacket packet) {
239		Content content = packet.getJingleContent();
240		String cid = content.getUsedCandidate();
241		IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
242		if (cid!=null) {
243			Log.d("xmppService","candidate used by counterpart:"+cid);
244			JingleCandidate candidate = getCandidate(cid);
245			candidate.flagAsUsedByCounterpart();
246			if (status == STATUS_ACCEPTED) {
247				this.connect();
248			} else {
249				Log.d("xmppService","ignoring because file is already in transmission");
250			}
251		} else if (content.hasCandidateError()) {
252			Log.d("xmppService","received candidate error");
253			this.receivedCandidateError = true;
254			if (status == STATUS_ACCEPTED) {
255				this.connect();
256			}
257		}
258		account.getXmppConnection().sendIqPacket(response, null);
259	}
260
261	private void connect() {
262		final SocksConnection connection = chooseConnection();
263		this.status = STATUS_TRANSMITTING;
264		final OnFileTransmitted callback = new OnFileTransmitted() {
265			
266			@Override
267			public void onFileTransmitted(JingleFile file) {
268				if (responder.equals(account.getFullJid())) {
269					sendSuccess();
270					mXmppConnectionService.markMessage(message, Message.STATUS_SEND);
271				}
272				Log.d("xmppService","sucessfully transmitted file. sha1:"+file.getSha1Sum());
273			}
274		};
275		if (connection.isProxy()&&(connection.getCandidate().isOurs())) {
276			Log.d("xmppService","candidate "+connection.getCandidate().getCid()+" was our proxy and needs activation");
277			IqPacket activation = new IqPacket(IqPacket.TYPE_SET);
278			activation.setTo(connection.getCandidate().getJid());
279			activation.query("http://jabber.org/protocol/bytestreams").setAttribute("sid", this.getSessionId());
280			activation.query().addChild("activate").setContent(this.getCounterPart());
281			this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() {
282				
283				@Override
284				public void onIqPacketReceived(Account account, IqPacket packet) {
285					Log.d("xmppService","activation result: "+packet.toString());
286					if (initiator.equals(account.getFullJid())) {
287						Log.d("xmppService","we were initiating. sending file");
288						connection.send(file,callback);
289					} else {
290						connection.receive(file,callback);
291						Log.d("xmppService","we were responding. receiving file");
292					}
293				}
294			});
295		} else {
296			if (initiator.equals(account.getFullJid())) {
297				Log.d("xmppService","we were initiating. sending file");
298				connection.send(file,callback);
299			} else {
300				Log.d("xmppService","we were responding. receiving file");
301				connection.receive(file,callback);
302			}
303		}
304	}
305	
306	private SocksConnection chooseConnection() {
307		Log.d("xmppService","choosing connection from "+this.connections.size()+" possibilties");
308		SocksConnection connection = null;
309		Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator();
310	    while (it.hasNext()) {
311	    	Entry<String, SocksConnection> pairs = it.next();
312	    	SocksConnection currentConnection = pairs.getValue();
313	    	Log.d("xmppService","comparing candidate: "+currentConnection.getCandidate().toString());
314	        if (currentConnection.isEstablished()&&(currentConnection.getCandidate().isUsedByCounterpart()||(!currentConnection.getCandidate().isOurs()))) {
315	        	Log.d("xmppService","is usable");
316	        	if (connection==null) {
317	        		connection = currentConnection;
318	        	} else {
319	        		if (connection.getCandidate().getPriority()<currentConnection.getCandidate().getPriority()) {
320	        			connection = currentConnection;
321	        		} else if (connection.getCandidate().getPriority()==currentConnection.getCandidate().getPriority()) {
322	        			Log.d("xmppService","found two candidates with same priority");
323	        			if (initiator.equals(account.getFullJid())) {
324	        				if (currentConnection.getCandidate().isOurs()) {
325	        					connection = currentConnection;
326	        				}
327	        			} else {
328	        				if (!currentConnection.getCandidate().isOurs()) {
329	        					connection = currentConnection;
330	        				}
331	        			}
332	        		}
333	        	}
334	        }
335	        it.remove();
336	    }
337	    Log.d("xmppService","chose candidate: "+connection.getCandidate().getHost());
338		return connection;
339	}
340
341	private void sendSuccess() {
342		JinglePacket packet = bootstrapPacket("session-terminate");
343		Reason reason = new Reason();
344		reason.addChild("success");
345		packet.setReason(reason);
346		Log.d("xmppService","sending success. "+packet.toString());
347		this.account.getXmppConnection().sendIqPacket(packet, responseListener);
348		this.disconnect();
349		this.status = STATUS_FINISHED;
350		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_RECIEVED);
351	}
352	
353	private void finish() {
354		this.status = STATUS_FINISHED;
355		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND);
356		this.disconnect();
357	}
358	
359	public void cancel() {
360		this.disconnect();
361		this.status = STATUS_CANCELED;
362		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_REJECTED);
363	}
364	
365	private void openOurCandidates() {
366		for(JingleCandidate candidate : this.candidates) {
367			if (candidate.isOurs()) {
368				final SocksConnection socksConnection = new SocksConnection(this,candidate);
369				connections.put(candidate.getCid(), socksConnection);
370				socksConnection.connect(new OnSocksConnection() {
371					
372					@Override
373					public void failed() {
374						Log.d("xmppService","connection to our candidate failed");
375					}
376					
377					@Override
378					public void established() {
379						Log.d("xmppService","connection to our candidate was successful");
380					}
381				});
382			}
383		}
384	}
385	
386	private void connectNextCandidate() {
387		for(JingleCandidate candidate : this.candidates) {
388			if ((!connections.containsKey(candidate.getCid())&&(!candidate.isOurs()))) {
389				this.connectWithCandidate(candidate);
390				return;
391			}
392		}
393		this.sendCandidateError();
394	}
395	
396	private void connectWithCandidate(final JingleCandidate candidate) {
397		final SocksConnection socksConnection = new SocksConnection(this,candidate);
398		connections.put(candidate.getCid(), socksConnection);
399		socksConnection.connect(new OnSocksConnection() {
400			
401			@Override
402			public void failed() {
403				connectNextCandidate();
404			}
405			
406			@Override
407			public void established() {
408				sendCandidateUsed(candidate.getCid());
409				if ((receivedCandidateError)&&(status == STATUS_ACCEPTED)) {
410					Log.d("xmppService","received candidate error before. trying to connect");
411					connect();
412				}
413			}
414		});
415	}
416
417	private void disconnect() {
418		Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator();
419	    while (it.hasNext()) {
420	        Entry<String, SocksConnection> pairs = it.next();
421	        pairs.getValue().disconnect();
422	        it.remove();
423	    }
424	}
425	
426	private void sendCandidateUsed(final String cid) {
427		JinglePacket packet = bootstrapPacket("transport-info");
428		Content content = new Content();
429		//TODO: put these into actual variables
430		content.setAttribute("creator", "initiator");
431		content.setAttribute("name", "a-file-offer");
432		content.setUsedCandidate(this.transportId, cid);
433		packet.setContent(content);
434		Log.d("xmppService","send using candidate: "+cid);
435		this.account.getXmppConnection().sendIqPacket(packet,responseListener);
436	}
437	
438	private void sendCandidateError() {
439		JinglePacket packet = bootstrapPacket("transport-info");
440		Content content = new Content();
441		//TODO: put these into actual variables
442		content.setAttribute("creator", "initiator");
443		content.setAttribute("name", "a-file-offer");
444		content.setCandidateError(this.transportId);
445		packet.setContent(content);
446		Log.d("xmppService","send candidate error");
447		this.account.getXmppConnection().sendIqPacket(packet,responseListener);
448	}
449
450	public String getInitiator() {
451		return this.initiator;
452	}
453	
454	public String getResponder() {
455		return this.responder;
456	}
457	
458	public int getStatus() {
459		return this.status;
460	}
461	
462	private boolean equalCandidateExists(JingleCandidate candidate) {
463		for(JingleCandidate c : this.candidates) {
464			if (c.equalValues(candidate)) {
465				return true;
466			}
467		}
468		return false;
469	}
470	
471	private void mergeCandidate(JingleCandidate candidate) {
472		for(JingleCandidate c : this.candidates) {
473			if (c.equals(candidate)) {
474				return;
475			}
476		}
477		this.candidates.add(candidate);
478	}
479	
480	private void mergeCandidates(List<JingleCandidate> candidates) {
481		for(JingleCandidate c : candidates) {
482			mergeCandidate(c);
483		}
484	}
485	
486	private JingleCandidate getCandidate(String cid) {
487		for(JingleCandidate c : this.candidates) {
488			if (c.getCid().equals(cid)) {
489				return c;
490			}
491		}
492		return null;
493	}
494}