001/**
002 *
003 * Copyright 2003-2006 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smackx.jingleold.nat;
019
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Iterator;
023import java.util.List;
024import java.util.logging.Level;
025import java.util.logging.Logger;
026
027import org.jivesoftware.smack.SmackException;
028import org.jivesoftware.smack.SmackException.NotConnectedException;
029import org.jivesoftware.smack.XMPPException;
030import org.jivesoftware.smack.packet.IQ;
031
032import org.jivesoftware.smackx.jingleold.ContentNegotiator;
033import org.jivesoftware.smackx.jingleold.JingleActionEnum;
034import org.jivesoftware.smackx.jingleold.JingleException;
035import org.jivesoftware.smackx.jingleold.JingleNegotiator;
036import org.jivesoftware.smackx.jingleold.JingleNegotiatorState;
037import org.jivesoftware.smackx.jingleold.JingleSession;
038import org.jivesoftware.smackx.jingleold.listeners.JingleListener;
039import org.jivesoftware.smackx.jingleold.listeners.JingleTransportListener;
040import org.jivesoftware.smackx.jingleold.nat.ICECandidate.Type;
041import org.jivesoftware.smackx.jingleold.packet.Jingle;
042import org.jivesoftware.smackx.jingleold.packet.JingleContent;
043import org.jivesoftware.smackx.jingleold.packet.JingleTransport;
044import org.jivesoftware.smackx.jingleold.packet.JingleTransport.JingleTransportCandidate;
045
046/**
047 * Transport negotiator.
048 *
049 * This class is responsible for managing the transport negotiation process,
050 * handling all the stanza interchange and the stage control.
051 *
052 * @author Alvaro Saurin
053 */
054@SuppressWarnings("UnusedVariable")
055public abstract class TransportNegotiator extends JingleNegotiator {
056
057    private static final Logger LOGGER = Logger.getLogger(TransportNegotiator.class.getName());
058
059    // The time we give to the candidates check before we accept or decline the
060    // transport (in milliseconds)
061    public static final int CANDIDATES_ACCEPT_PERIOD = 4000;
062
063    // The session this negotiator belongs to
064    // private final JingleSession session;
065
066    // The transport manager
067    private final TransportResolver resolver;
068
069    // Transport candidates we have offered
070    private final List<TransportCandidate> offeredCandidates = new ArrayList<>();
071
072    // List of remote transport candidates
073    private final List<TransportCandidate> remoteCandidates = new ArrayList<>();
074
075    // Valid remote candidates
076    private final List<TransportCandidate> validRemoteCandidates = new ArrayList<>();
077
078    // Accepted Remote Candidates
079    private final List<TransportCandidate> acceptedRemoteCandidates = new ArrayList<>();
080
081    // The best local candidate we have offered (and accepted by the other part)
082    private TransportCandidate acceptedLocalCandidate;
083
084    // The thread that will report the result to the other end
085    private Thread resultThread;
086
087    // Listener for the resolver
088    private TransportResolverListener.Resolver resolverListener;
089
090    private final ContentNegotiator parentNegotiator;
091
092    /**
093    * Default constructor.
094    *
095    * @param session            The Jingle session
096    * @param transResolver The JingleTransportManager to use
097    * @param parentNegotiator the parent ngeotiator.
098    */
099    public TransportNegotiator(JingleSession session, TransportResolver transResolver, ContentNegotiator parentNegotiator) {
100        super(session);
101
102        resolver = transResolver;
103        this.parentNegotiator = parentNegotiator;
104
105        resultThread = null;
106    }
107
108    /**
109     * Get a new instance of the right TransportNegotiator class with this
110     * candidate.
111     *
112     * @param cand the transport candidate.
113     * @return A TransportNegotiator instance
114     */
115    public abstract JingleTransport getJingleTransport(TransportCandidate cand);
116
117    /**
118     * Return true if the transport candidate is acceptable for the current
119     * negotiator.
120     *
121     * @param tc the transport candidate.
122     * @param localCandidates a list of local transport candidates.
123     * @return true if the transport candidate is acceptable
124     */
125    public abstract boolean acceptableTransportCandidate(TransportCandidate tc, List<TransportCandidate> localCandidates);
126
127    /**
128     * Obtain the best local candidate we want to offer.
129     *
130     * @return the best local candidate
131     */
132    public final TransportCandidate getBestLocalCandidate() {
133        return resolver.getPreferredCandidate();
134    }
135
136    /**
137     * Set the best local transport candidate we have offered and accepted by
138     * the other endpoint.
139     *
140     * @param bestLocalCandidate the acceptedLocalCandidate to set
141     */
142    private void setAcceptedLocalCandidate(TransportCandidate bestLocalCandidate) {
143        for (int i = 0; i < resolver.getCandidateCount(); i++) {
144            // TODO FIX The EQUAL Sentence
145            if (resolver.getCandidate(i).getIp().equals(bestLocalCandidate.getIp())
146                    && resolver.getCandidate(i).getPort() == bestLocalCandidate.getPort()) {
147                acceptedLocalCandidate = resolver.getCandidate(i);
148                return;
149            }
150        }
151        LOGGER.fine("BEST: ip=" + bestLocalCandidate.getIp() + " port=" + bestLocalCandidate.getPort() + " has not been offered.");
152        // throw new XMPPException("Local transport candidate has not be offered.");
153    }
154
155    /**
156     * Get the best accepted local candidate we have offered.
157     *
158     * @return a transport candidate we have offered.
159     */
160    public TransportCandidate getAcceptedLocalCandidate() {
161        return acceptedLocalCandidate;
162    }
163
164    /**
165     *  Called from above to start the negotiator during a session-initiate.
166     */
167    @Override
168    protected void doStart() {
169
170        try {
171            sendTransportCandidatesOffer();
172            setNegotiatorState(JingleNegotiatorState.PENDING);
173        } catch (Exception e) {
174            // TODO Auto-generated catch block
175            LOGGER.log(Level.WARNING, "exception", e);
176        }
177
178    }
179
180    /**
181     * Called from above to session-terminate.
182     */
183    @Override
184    public void close() {
185        super.close();
186
187    }
188
189    /**
190     *  Return a JingleTransport that best reflects this transport negotiator.
191     *
192     *  @return the jingle transport.
193     */
194    public JingleTransport getJingleTransport() {
195        return getJingleTransport(getBestRemoteCandidate());
196    }
197
198    public List<TransportCandidate> getOfferedCandidates() {
199        return offeredCandidates;
200    }
201
202    /**
203     * Obtain the best common transport candidate obtained in the negotiation.
204     *
205     * @return the bestRemoteCandidate
206     */
207    public abstract TransportCandidate getBestRemoteCandidate();
208
209    /**
210     * Get the list of remote candidates.
211     *
212     * @return the remoteCandidates
213     */
214    private List<TransportCandidate> getRemoteCandidates() {
215        return remoteCandidates;
216    }
217
218    /**
219     * Add a remote candidate to the list. The candidate will be checked in
220     * order to verify if it is usable.
221     *
222     * @param rc a remote candidate to add and check.
223     */
224    private void addRemoteCandidate(TransportCandidate rc) {
225        // Add the candidate to the list
226        if (rc != null) {
227            if (acceptableTransportCandidate(rc, offeredCandidates)) {
228                synchronized (remoteCandidates) {
229                    remoteCandidates.add(rc);
230                }
231
232                // Check if the new candidate can be used.
233                checkRemoteCandidate(rc);
234            }
235        }
236    }
237
238    /**
239     * Add a offered candidate to the list.
240     *
241     * @param rc a remote candidate we have offered.
242     */
243    private void addOfferedCandidate(TransportCandidate rc) {
244        // Add the candidate to the list
245        if (rc != null) {
246            synchronized (offeredCandidates) {
247                offeredCandidates.add(rc);
248            }
249        }
250    }
251
252    /**
253     * Check asynchronously the new transport candidate.
254     *
255     * @param offeredCandidate a transport candidates to check
256     */
257    private void checkRemoteCandidate(final TransportCandidate offeredCandidate) {
258        offeredCandidate.addListener(new TransportResolverListener.Checker() {
259            @Override
260            public void candidateChecked(TransportCandidate cand, final boolean validCandidate) {
261                if (validCandidate) {
262                    if (getNegotiatorState() == JingleNegotiatorState.PENDING)
263                        addValidRemoteCandidate(offeredCandidate);
264                }
265            }
266
267            @Override
268            public void candidateChecking(TransportCandidate cand) {
269            }
270
271        });
272        offeredCandidate.check(resolver.getCandidatesList());
273    }
274
275    /**
276     * Return true if the transport is established.
277     *
278     * @return true if the transport is established.
279     */
280    private boolean isEstablished() {
281        return getBestRemoteCandidate() != null && getAcceptedLocalCandidate() != null;
282    }
283
284    /**
285     * Return true if the transport is fully established.
286     *
287     * @return true if the transport is fully established.
288     */
289    public final boolean isFullyEstablished() {
290        return isEstablished() && ((getNegotiatorState() == JingleNegotiatorState.SUCCEEDED) || (getNegotiatorState() == JingleNegotiatorState.FAILED));
291    }
292
293    /**
294     * Launch a thread that checks, after some time, if any of the candidates
295     * offered by the other endpoint is usable. The thread does not check the
296     * candidates: it just checks if we have got a valid one and sends an Accept
297     * in that case.
298     */
299    private void delayedCheckBestCandidate(final JingleSession js, final Jingle jin) {
300        //
301        // If this is the first insertion in the list, start the thread that
302        // will send the result of our checks...
303        //
304        if (resultThread == null && !getRemoteCandidates().isEmpty()) {
305            resultThread = new Thread(new Runnable() {
306
307                @Override
308                public void run() {
309
310                    // Sleep for some time, waiting for the candidates checks
311
312                    int totalTime = CANDIDATES_ACCEPT_PERIOD + TransportResolver.CHECK_TIMEOUT;
313                    int tries = (int) Math.ceil(totalTime / 1000);
314
315                    for (int i = 0; i < tries - 1; i++) {
316                        try {
317                            Thread.sleep(1000);
318                        } catch (InterruptedException e) {
319                            LOGGER.log(Level.WARNING, "exception", e);
320                        }
321
322                        // Once we are in pending state, look for any valid remote
323                        // candidate, and send an "accept" if we have one...
324                        TransportCandidate bestRemote = getBestRemoteCandidate();
325                        // State state = getState();
326
327                        if (bestRemote != null
328                                && getNegotiatorState() == JingleNegotiatorState.PENDING) {
329                            // Accepting the remote candidate
330                            if (!acceptedRemoteCandidates.contains(bestRemote)) {
331                                Jingle jout = new Jingle(JingleActionEnum.CONTENT_ACCEPT);
332                                JingleContent content = parentNegotiator.getJingleContent();
333                                content.addJingleTransport(getJingleTransport(bestRemote));
334                                jout.addContent(content);
335
336                                // Send the packet
337                                try {
338                                    js.sendFormattedJingle(jin, jout);
339                                }
340                                catch (InterruptedException | NotConnectedException e) {
341                                    throw new IllegalStateException(e);
342                                }
343                                acceptedRemoteCandidates.add(bestRemote);
344                            }
345                            if (isEstablished() && getNegotiatorState() == JingleNegotiatorState.PENDING) {
346                                setNegotiatorState(JingleNegotiatorState.SUCCEEDED);
347                                try {
348                                    triggerTransportEstablished(getAcceptedLocalCandidate(), bestRemote);
349                                }
350                                catch (InterruptedException | NotConnectedException e) {
351                                    throw new IllegalStateException(e);
352                                }
353                                break;
354                            }
355                        }
356                    }
357
358                    // Once we are in pending state, look for any valid remote
359                    // candidate, and send an "accept" if we have one...
360                    TransportCandidate bestRemote = getBestRemoteCandidate();
361
362                    if (bestRemote == null) {
363                        boolean foundRemoteRelay = false;
364                        for (TransportCandidate candidate : remoteCandidates) {
365                            if (candidate instanceof ICECandidate) {
366                                ICECandidate iceCandidate = (ICECandidate) candidate;
367                                if (iceCandidate.getType().equals(Type.relay)) {
368                                    // TODO Check if the relay is reachable.
369                                    addValidRemoteCandidate(iceCandidate);
370                                    foundRemoteRelay = true;
371                                }
372                            }
373                        }
374
375                        // If not found, check if we offered a relay. If yes, we should accept any remote candidate.
376                        // We should accept the Public One if we received it, otherwise, accepts any.
377                        if (!foundRemoteRelay) {
378                            boolean foundLocalRelay = false;
379                            for (TransportCandidate candidate : offeredCandidates) {
380                                if (candidate instanceof ICECandidate) {
381                                    ICECandidate iceCandidate = (ICECandidate) candidate;
382                                    if (iceCandidate.getType().equals(Type.relay)) {
383                                        foundLocalRelay = true;
384                                    }
385                                }
386                            }
387                            if (foundLocalRelay) {
388                                boolean foundRemotePublic = false;
389                                for (TransportCandidate candidate : remoteCandidates) {
390                                    if (candidate instanceof ICECandidate) {
391                                        ICECandidate iceCandidate = (ICECandidate) candidate;
392                                        if (iceCandidate.getType().equals(ICECandidate.Type.srflx)) {
393                                            addValidRemoteCandidate(iceCandidate);
394                                            foundRemotePublic = true;
395                                        }
396                                    }
397                                }
398                                if (!foundRemotePublic) {
399                                    for (TransportCandidate candidate : remoteCandidates) {
400                                        if (candidate instanceof ICECandidate) {
401                                            ICECandidate iceCandidate = (ICECandidate) candidate;
402                                            addValidRemoteCandidate(iceCandidate);
403                                        }
404                                    }
405                                }
406                            }
407                        }
408                    }
409
410                    for (int i = 0; i < 6; i++) {
411                        try {
412                            Thread.sleep(500);
413                        } catch (InterruptedException e) {
414                            LOGGER.log(Level.WARNING, "exception", e);
415                        }
416
417                        bestRemote = getBestRemoteCandidate();
418                        // State state = getState();
419                        if (bestRemote != null
420                                && getNegotiatorState() == JingleNegotiatorState.PENDING) {
421                            if (!acceptedRemoteCandidates.contains(bestRemote)) {
422                                Jingle jout = new Jingle(JingleActionEnum.CONTENT_ACCEPT);
423                                JingleContent content = parentNegotiator.getJingleContent();
424                                content.addJingleTransport(getJingleTransport(bestRemote));
425                                jout.addContent(content);
426
427                                // Send the packet
428                                try {
429                                    js.sendFormattedJingle(jin, jout);
430                                }
431                                catch (InterruptedException | NotConnectedException e) {
432                                    throw new IllegalStateException(e);
433                                }
434                                acceptedRemoteCandidates.add(bestRemote);
435                            }
436                            if (isEstablished()) {
437                                setNegotiatorState(JingleNegotiatorState.SUCCEEDED);
438                                break;
439                            }
440                        }
441                    }
442
443                    if (getNegotiatorState() != JingleNegotiatorState.SUCCEEDED) {
444                        try {
445                            session
446                                    .terminate("Unable to negotiate session. This may be caused by firewall configuration problems.");
447                        } catch (Exception e) {
448                            LOGGER.log(Level.WARNING, "exception", e);
449                        }
450                    }
451                }
452            }, "Waiting for all the transport candidates checks...");
453
454            resultThread.setName("Transport Resolver Result");
455            resultThread.start();
456        }
457    }
458
459    /**
460     * Add a valid remote candidate to the list. The remote candidate has been
461     * checked, and the remote
462     *
463     * @param remoteCandidate a remote candidate to add
464     */
465    private void addValidRemoteCandidate(TransportCandidate remoteCandidate) {
466        // Add the candidate to the list
467        if (remoteCandidate != null) {
468            synchronized (validRemoteCandidates) {
469                LOGGER.fine("Added valid candidate: " + remoteCandidate.getIp() + ":" + remoteCandidate.getPort());
470                validRemoteCandidates.add(remoteCandidate);
471            }
472        }
473    }
474
475    /**
476     * Get the list of valid (ie, checked) remote candidates.
477     *
478     * @return The list of valid (ie, already checked) remote candidates.
479     */
480    final ArrayList<TransportCandidate> getValidRemoteCandidatesList() {
481        synchronized (validRemoteCandidates) {
482            return new ArrayList<>(validRemoteCandidates);
483        }
484    }
485
486    /**
487     * Get an iterator for the list of valid (ie, checked) remote candidates.
488     *
489     * @return The iterator for the list of valid (ie, already checked) remote
490     *         candidates.
491     */
492    public final Iterator<TransportCandidate> getValidRemoteCandidates() {
493        return Collections.unmodifiableList(getRemoteCandidates()).iterator();
494    }
495
496    /**
497     * Add an offered remote candidate. The transport candidate can be unusable:
498     * we must check if we can use it.
499     *
500     * @param rc the remote candidate to add.
501     */
502    private void addRemoteCandidates(List<TransportCandidate> rc) {
503        if (rc != null) {
504            if (rc.size() > 0) {
505                for (TransportCandidate aRc : rc) {
506                    addRemoteCandidate(aRc);
507                }
508            }
509        }
510    }
511
512    /**
513     * Parse the list of transport candidates from a Jingle packet.
514     *
515     * @param jin The input jingle packet
516     */
517    private List<TransportCandidate> obtainCandidatesList(Jingle jingle) {
518        List<TransportCandidate> result = new ArrayList<>();
519
520        if (jingle != null) {
521            // Get the list of candidates from the packet
522            for (JingleContent jingleContent : jingle.getContentsList()) {
523                if (jingleContent.getName().equals(parentNegotiator.getName())) {
524                    for (JingleTransport jingleTransport : jingleContent.getJingleTransportsList()) {
525                        for (JingleTransportCandidate jingleTransportCandidate : jingleTransport.getCandidatesList()) {
526                            TransportCandidate transCand = jingleTransportCandidate.getMediaTransport();
527                            result.add(transCand);
528                        }
529                    }
530                }
531            }
532        }
533
534        return result;
535    }
536
537    /**
538     * Send an offer for a transport candidate
539     *
540     * @param cand TODO javadoc me please
541     * @throws NotConnectedException if the XMPP connection is not connected.
542     * @throws InterruptedException if the calling thread was interrupted.
543     */
544    private synchronized void sendTransportCandidateOffer(TransportCandidate cand) throws NotConnectedException, InterruptedException {
545        if (!cand.isNull()) {
546            // Offer our new candidate...
547            addOfferedCandidate(cand);
548            JingleContent content = parentNegotiator.getJingleContent();
549            content.addJingleTransport(getJingleTransport(cand));
550            Jingle jingle = new Jingle(JingleActionEnum.TRANSPORT_INFO);
551            jingle.addContent(content);
552
553            // We SHOULD NOT be sending packets directly.
554            // This circumvents the state machinery.
555            // TODO - work this into the state machinery.
556            session.sendFormattedJingle(jingle);
557        }
558    }
559
560    /**
561     * Create a Jingle stanza where we announce our transport candidates.
562     *
563     * @throws XMPPException if an XMPP protocol error was received.
564     * @throws SmackException if Smack detected an exceptional situation.
565     * @throws InterruptedException if the calling thread was interrupted.
566     */
567    private void sendTransportCandidatesOffer() throws XMPPException, SmackException, InterruptedException {
568        List<TransportCandidate> notOffered = resolver.getCandidatesList();
569
570        notOffered.removeAll(offeredCandidates);
571
572        // Send any unset candidate
573        for (Object aNotOffered : notOffered) {
574            sendTransportCandidateOffer((TransportCandidate) aNotOffered);
575        }
576
577        // .. and start a listener that will send any future candidate
578        if (resolverListener == null) {
579            // Add a listener that sends the offer when the resolver finishes...
580            resolverListener = new TransportResolverListener.Resolver() {
581                @Override
582                public void candidateAdded(TransportCandidate cand) throws NotConnectedException, InterruptedException {
583                    sendTransportCandidateOffer(cand);
584                }
585
586                @Override
587                public void end() {
588                }
589
590                @Override
591                public void init() {
592                }
593            };
594
595            resolver.addListener(resolverListener);
596        }
597
598        if (!(resolver.isResolving() || resolver.isResolved())) {
599            // Resolve our IP and port
600            LOGGER.fine("RESOLVER CALLED");
601            resolver.resolve(session);
602        }
603    }
604
605    /**
606     * Dispatch an incoming packet. The method is responsible for recognizing
607     * the stanza type and, depending on the current state, delivering the
608     * stanza to the right event handler and wait for a response.
609     *
610     * @param iq the stanza received
611     * @return the new Jingle stanza to send.
612     * @throws XMPPException if an XMPP protocol error was received.
613     * @throws SmackException if Smack detected an exceptional situation.
614     * @throws InterruptedException if the calling thread was interrupted.
615     */
616    @Override
617    public final List<IQ> dispatchIncomingPacket(IQ iq, String id) throws XMPPException, SmackException, InterruptedException {
618        List<IQ> responses = new ArrayList<>();
619        IQ response = null;
620
621        if (iq != null) {
622            if (iq.getType().equals(IQ.Type.error)) {
623                // Process errors
624                setNegotiatorState(JingleNegotiatorState.FAILED);
625                triggerTransportClosed(null);
626                // This next line seems wrong, and may subvert the normal closing process.
627                throw new JingleException(iq.getError().getDescriptiveText());
628            } else if (iq.getType().equals(IQ.Type.result)) {
629                // Process ACKs
630                if (isExpectedId(iq.getStanzaId())) {
631                    response = receiveResult(iq);
632                    removeExpectedId(iq.getStanzaId());
633                }
634            } else if (iq instanceof Jingle) {
635                // Get the action from the Jingle packet
636                Jingle jingle = (Jingle) iq;
637                JingleActionEnum action = jingle.getAction();
638
639                switch (action) {
640                    case CONTENT_ACCEPT:
641                        response = receiveContentAcceptAction(jingle);
642                        break;
643
644                    case CONTENT_MODIFY:
645                        break;
646
647                    case CONTENT_REMOVE:
648                        break;
649
650                    case SESSION_INFO:
651                        break;
652
653                    case SESSION_INITIATE:
654                        response = receiveSessionInitiateAction(jingle);
655                        break;
656
657                    case SESSION_ACCEPT:
658                        response = receiveSessionAcceptAction(jingle);
659                        break;
660
661                    case TRANSPORT_INFO:
662                        response = receiveTransportInfoAction(jingle);
663                        break;
664
665                    default:
666                        break;
667                }
668            }
669        }
670
671        if (response != null) {
672            addExpectedId(response.getStanzaId());
673            responses.add(response);
674        }
675
676        return responses;
677    }
678
679    /**
680     * The other endpoint has partially accepted our invitation: start
681     * offering a list of candidates.
682     *
683     * @return an IQ packet
684     * @throws XMPPException if an XMPP protocol error was received.
685     * @throws SmackException if Smack detected an exceptional situation.
686     * @throws InterruptedException if the calling thread was interrupted.
687     */
688    private Jingle receiveResult(IQ iq) throws XMPPException, SmackException, InterruptedException {
689        Jingle response = null;
690
691        sendTransportCandidatesOffer();
692        setNegotiatorState(JingleNegotiatorState.PENDING);
693
694        return response;
695    }
696
697    /**
698     *  @param jingle TODO javadoc me please
699     *  @return the iq
700     * @throws SmackException if Smack detected an exceptional situation.
701     * @throws InterruptedException if the calling thread was interrupted.
702     */
703    private IQ receiveSessionInitiateAction(Jingle jingle) throws XMPPException, SmackException, InterruptedException {
704        IQ response = null;
705
706        // Parse the Jingle and get any proposed transport candidates
707        // addRemoteCandidates(obtainCandidatesList(jin));
708
709        // Start offering candidates
710        sendTransportCandidatesOffer();
711
712        // All these candidates will be checked asynchronously. Wait for some
713        // time and check if we have a valid candidate to use...
714        delayedCheckBestCandidate(session, jingle);
715
716        // Set the next state
717        setNegotiatorState(JingleNegotiatorState.PENDING);
718
719        return response;
720    }
721
722    /**
723     *  @param jingle TODO javadoc me please
724     *  @return the iq
725     */
726    private IQ receiveTransportInfoAction(Jingle jingle) {
727        IQ response = null;
728
729        // Parse the Jingle and get any proposed transport candidates
730        // addRemoteCandidates(obtainCandidatesList(jin));
731
732        //        // Start offering candidates
733        //        sendTransportCandidatesOffer();
734        //
735        //        // All these candidates will be checked asynchronously. Wait for some
736        //        // time and check if we have a valid candidate to use...
737        //        delayedCheckBestCandidate(session, jingle);
738        //
739        //        // Set the next state
740        //        setNegotiatorState(JingleNegotiatorState.PENDING);
741
742        // Parse the Jingle and get any proposed transport candidates
743        addRemoteCandidates(obtainCandidatesList(jingle));
744
745        // Wait for some time and check if we have a valid candidate to
746        // use...
747        delayedCheckBestCandidate(session, jingle);
748
749        response = session.createAck(jingle);
750
751        return response;
752    }
753
754    /**
755     * One of our transport candidates has been accepted.
756     *
757     * @return a Jingle packet
758     * @throws XMPPException an exception
759     * @see org.jivesoftware.smackx.jingleold.JingleNegotiator.State#eventAccept(org.jivesoftware.smackx.jingleold.packet.Jingle)
760     */
761    private IQ receiveContentAcceptAction(Jingle jingle) throws XMPPException {
762        IQ response = null;
763
764        // Parse the Jingle and get the accepted candidate
765        List<TransportCandidate> accepted = obtainCandidatesList(jingle);
766        if (!accepted.isEmpty()) {
767
768            for (TransportCandidate cand : accepted) {
769                LOGGER.fine("Remote accepted candidate addr: " + cand.getIp());
770            }
771
772            TransportCandidate cand = accepted.get(0);
773            setAcceptedLocalCandidate(cand);
774
775            if (isEstablished()) {
776                LOGGER.fine(cand.getIp() + " is set active");
777                // setNegotiatorState(JingleNegotiatorState.SUCCEEDED);
778            }
779        }
780        return response;
781    }
782
783    /**
784     *  @param jingle TODO javadoc me please
785     *  @return the iq
786     */
787    private static IQ receiveSessionAcceptAction(Jingle jingle) {
788        IQ response = null;
789
790        LOGGER.fine("Transport established");
791        // triggerTransportEstablished(getAcceptedLocalCandidate(), getBestRemoteCandidate());
792
793        // setNegotiatorState(JingleNegotiatorState.SUCCEEDED);
794
795        return response;
796    }
797
798    /**
799     * Trigger a Transport session established event.
800     *
801     * @param local  TransportCandidate that has been agreed.
802     * @param remote TransportCandidate that has been agreed.
803     * @throws NotConnectedException if the XMPP connection is not connected.
804     * @throws InterruptedException if the calling thread was interrupted.
805     */
806    private void triggerTransportEstablished(TransportCandidate local, TransportCandidate remote) throws NotConnectedException, InterruptedException {
807        List<JingleListener> listeners = getListenersList();
808        for (JingleListener li : listeners) {
809            if (li instanceof JingleTransportListener) {
810                JingleTransportListener mli = (JingleTransportListener) li;
811                LOGGER.fine("triggerTransportEstablished " + local.getLocalIp() + ":" + local.getPort() + " <-> "
812                        + remote.getIp() + ":" + remote.getPort());
813                mli.transportEstablished(local, remote);
814            }
815        }
816    }
817
818    /**
819     * Trigger a Transport closed event.
820     *
821     * @param cand current TransportCandidate that is cancelled.
822     */
823    private void triggerTransportClosed(TransportCandidate cand) {
824        List<JingleListener> listeners = getListenersList();
825        for (JingleListener li : listeners) {
826            if (li instanceof JingleTransportListener) {
827                JingleTransportListener mli = (JingleTransportListener) li;
828                mli.transportClosed(cand);
829            }
830        }
831    }
832
833    // Subclasses
834
835    /**
836     * Raw-UDP transport negotiator.
837     *
838     * @author Alvaro Saurin
839     */
840    public static final class RawUdp extends TransportNegotiator {
841
842        /**
843         * Default constructor, with a JingleSession and transport manager.
844         *
845         * @param js  The Jingle session this negotiation belongs to.
846         * @param res The transport resolver to use.
847         * @param parentNegotiator the parent content negotiator.
848         */
849        public RawUdp(JingleSession js, final TransportResolver res, ContentNegotiator parentNegotiator) {
850            super(js, res, parentNegotiator);
851        }
852
853        /**
854         * Get a TransportNegotiator instance.
855         */
856        @Override
857        public org.jivesoftware.smackx.jingleold.packet.JingleTransport getJingleTransport(TransportCandidate bestRemote) {
858            org.jivesoftware.smackx.jingleold.packet.JingleTransport.RawUdp jt = new org.jivesoftware.smackx.jingleold.packet.JingleTransport.RawUdp();
859            jt.addCandidate(new org.jivesoftware.smackx.jingleold.packet.JingleTransport.RawUdp.Candidate(bestRemote));
860            return jt;
861        }
862
863        /**
864         * Obtain the best common transport candidate obtained in the
865         * negotiation.
866         *
867         * @return the bestRemoteCandidate
868         */
869        @Override
870        public TransportCandidate getBestRemoteCandidate() {
871            // Hopefully, we only have one validRemoteCandidate
872            ArrayList<TransportCandidate> cands = getValidRemoteCandidatesList();
873            if (!cands.isEmpty()) {
874                LOGGER.fine("RAW CAND");
875                return cands.get(0);
876            } else {
877                LOGGER.fine("No Remote Candidate");
878                return null;
879            }
880        }
881
882        /**
883         * Return true for fixed candidates.
884         */
885        @Override
886        public boolean acceptableTransportCandidate(TransportCandidate tc, List<TransportCandidate> localCandidates) {
887            return tc instanceof TransportCandidate.Fixed;
888        }
889    }
890
891    /**
892     * Ice transport negotiator.
893     *
894     * @author Alvaro Saurin
895     */
896    public static final class Ice extends TransportNegotiator {
897
898        /**
899         * Default constructor, with a JingleSession and transport manager.
900         *
901         * @param js  The Jingle session this negotiation belongs to.
902         * @param res The transport manager to use.
903         * @param parentNegotiator the parent content negotiator.
904         */
905        public Ice(JingleSession js, final TransportResolver res, ContentNegotiator parentNegotiator) {
906            super(js, res, parentNegotiator);
907        }
908
909        /**
910         * Get a TransportNegotiator instance.
911         *
912         * @param candidate TODO javadoc me please
913         */
914        @Override
915        public org.jivesoftware.smackx.jingleold.packet.JingleTransport getJingleTransport(TransportCandidate candidate) {
916            org.jivesoftware.smackx.jingleold.packet.JingleTransport.Ice jt = new org.jivesoftware.smackx.jingleold.packet.JingleTransport.Ice();
917            jt.addCandidate(new org.jivesoftware.smackx.jingleold.packet.JingleTransport.Ice.Candidate(candidate));
918            return jt;
919        }
920
921        /**
922         * Obtain the best remote candidate obtained in the negotiation so far.
923         *
924         * @return the bestRemoteCandidate
925         */
926        @Override
927        public TransportCandidate getBestRemoteCandidate() {
928            ICECandidate result = null;
929
930            ArrayList<TransportCandidate> cands = getValidRemoteCandidatesList();
931            if (!cands.isEmpty()) {
932                int highest = -1;
933                ICECandidate chose = null;
934                for (TransportCandidate transportCandidate : cands) {
935                    if (transportCandidate instanceof ICECandidate) {
936                        ICECandidate icecandidate = (ICECandidate) transportCandidate;
937                        if (icecandidate.getPreference() > highest) {
938                            chose = icecandidate;
939                            highest = icecandidate.getPreference();
940                        }
941                    }
942                }
943                result = chose;
944            }
945
946            if (result != null && result.getType().equals(Type.relay))
947                LOGGER.fine("Relay Type");
948
949            return result;
950        }
951
952        /**
953         * Return true for ICE candidates.
954         */
955        @Override
956        public boolean acceptableTransportCandidate(TransportCandidate tc, List<TransportCandidate> localCandidates) {
957            return tc instanceof ICECandidate;
958        }
959    }
960}