001/**
002 *
003 * Copyright the original author or authors
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 */
017package org.jivesoftware.smackx.bytestreams.ibb;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.net.SocketTimeoutException;
023import java.util.concurrent.BlockingQueue;
024import java.util.concurrent.LinkedBlockingQueue;
025import java.util.concurrent.TimeUnit;
026
027import org.jivesoftware.smack.SmackException.NotConnectedException;
028import org.jivesoftware.smack.SmackException.NotLoggedInException;
029import org.jivesoftware.smack.StanzaListener;
030import org.jivesoftware.smack.XMPPConnection;
031import org.jivesoftware.smack.filter.AndFilter;
032import org.jivesoftware.smack.filter.StanzaFilter;
033import org.jivesoftware.smack.filter.StanzaTypeFilter;
034import org.jivesoftware.smack.packet.IQ;
035import org.jivesoftware.smack.packet.Message;
036import org.jivesoftware.smack.packet.Stanza;
037import org.jivesoftware.smack.packet.XMPPError;
038import org.jivesoftware.smack.util.stringencoder.Base64;
039
040import org.jivesoftware.smackx.bytestreams.BytestreamSession;
041import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
042import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;
043import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
044import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
045
046import org.jxmpp.jid.Jid;
047
048/**
049 * InBandBytestreamSession class represents an In-Band Bytestream session.
050 * <p>
051 * In-band bytestreams are bidirectional and this session encapsulates the streams for both
052 * directions.
053 * <p>
054 * Note that closing the In-Band Bytestream session will close both streams. If both streams are
055 * closed individually the session will be closed automatically once the second stream is closed.
056 * Use the {@link #setCloseBothStreamsEnabled(boolean)} method if both streams should be closed
057 * automatically if one of them is closed.
058 * 
059 * @author Henning Staib
060 */
061public class InBandBytestreamSession implements BytestreamSession {
062
063    /* XMPP connection */
064    private final XMPPConnection connection;
065
066    /* the In-Band Bytestream open request for this session */
067    private final Open byteStreamRequest;
068
069    /*
070     * the input stream for this session (either IQIBBInputStream or MessageIBBInputStream)
071     */
072    private IBBInputStream inputStream;
073
074    /*
075     * the output stream for this session (either IQIBBOutputStream or MessageIBBOutputStream)
076     */
077    private IBBOutputStream outputStream;
078
079    /* JID of the remote peer */
080    private Jid remoteJID;
081
082    /* flag to close both streams if one of them is closed */
083    private boolean closeBothStreamsEnabled = false;
084
085    /* flag to indicate if session is closed */
086    private boolean isClosed = false;
087
088    /**
089     * Constructor.
090     * 
091     * @param connection the XMPP connection
092     * @param byteStreamRequest the In-Band Bytestream open request for this session
093     * @param remoteJID JID of the remote peer
094     */
095    protected InBandBytestreamSession(XMPPConnection connection, Open byteStreamRequest,
096                    Jid remoteJID) {
097        this.connection = connection;
098        this.byteStreamRequest = byteStreamRequest;
099        this.remoteJID = remoteJID;
100
101        // initialize streams dependent to the uses stanza type
102        switch (byteStreamRequest.getStanza()) {
103        case IQ:
104            this.inputStream = new IQIBBInputStream();
105            this.outputStream = new IQIBBOutputStream();
106            break;
107        case MESSAGE:
108            this.inputStream = new MessageIBBInputStream();
109            this.outputStream = new MessageIBBOutputStream();
110            break;
111        }
112
113    }
114
115    @Override
116    public InputStream getInputStream() {
117        return this.inputStream;
118    }
119
120    @Override
121    public OutputStream getOutputStream() {
122        return this.outputStream;
123    }
124
125    @Override
126    public int getReadTimeout() {
127        return this.inputStream.readTimeout;
128    }
129
130    @Override
131    public void setReadTimeout(int timeout) {
132        if (timeout < 0) {
133            throw new IllegalArgumentException("Timeout must be >= 0");
134        }
135        this.inputStream.readTimeout = timeout;
136    }
137
138    /**
139     * Returns whether both streams should be closed automatically if one of the streams is closed.
140     * Default is <code>false</code>.
141     * 
142     * @return <code>true</code> if both streams will be closed if one of the streams is closed,
143     *         <code>false</code> if both streams can be closed independently.
144     */
145    public boolean isCloseBothStreamsEnabled() {
146        return closeBothStreamsEnabled;
147    }
148
149    /**
150     * Sets whether both streams should be closed automatically if one of the streams is closed.
151     * Default is <code>false</code>.
152     * 
153     * @param closeBothStreamsEnabled <code>true</code> if both streams should be closed if one of
154     *        the streams is closed, <code>false</code> if both streams should be closed
155     *        independently
156     */
157    public void setCloseBothStreamsEnabled(boolean closeBothStreamsEnabled) {
158        this.closeBothStreamsEnabled = closeBothStreamsEnabled;
159    }
160
161    @Override
162    public void close() throws IOException {
163        closeByLocal(true); // close input stream
164        closeByLocal(false); // close output stream
165    }
166
167    /**
168     * This method is invoked if a request to close the In-Band Bytestream has been received.
169     * 
170     * @param closeRequest the close request from the remote peer
171     * @throws NotConnectedException 
172     * @throws InterruptedException 
173     */
174    protected void closeByPeer(Close closeRequest) throws NotConnectedException, InterruptedException {
175
176        /*
177         * close streams without flushing them, because stream is already considered closed on the
178         * remote peers side
179         */
180        this.inputStream.closeInternal();
181        this.inputStream.cleanup();
182        this.outputStream.closeInternal(false);
183
184        // acknowledge close request
185        IQ confirmClose = IQ.createResultIQ(closeRequest);
186        this.connection.sendStanza(confirmClose);
187
188    }
189
190    /**
191     * This method is invoked if one of the streams has been closed locally, if an error occurred
192     * locally or if the whole session should be closed.
193     * 
194     * @throws IOException if an error occurs while sending the close request
195     */
196    protected synchronized void closeByLocal(boolean in) throws IOException {
197        if (this.isClosed) {
198            return;
199        }
200
201        if (this.closeBothStreamsEnabled) {
202            this.inputStream.closeInternal();
203            this.outputStream.closeInternal(true);
204        }
205        else {
206            if (in) {
207                this.inputStream.closeInternal();
208            }
209            else {
210                // close stream but try to send any data left
211                this.outputStream.closeInternal(true);
212            }
213        }
214
215        if (this.inputStream.isClosed && this.outputStream.isClosed) {
216            this.isClosed = true;
217
218            // send close request
219            Close close = new Close(this.byteStreamRequest.getSessionID());
220            close.setTo(this.remoteJID);
221            try {
222                connection.createStanzaCollectorAndSend(close).nextResultOrThrow();
223            }
224            catch (Exception e) {
225                // Sadly we are unable to use the IOException(Throwable) constructor because this
226                // constructor is only supported from Android API 9 on.
227                IOException ioException = new IOException();
228                ioException.initCause(e);
229                throw ioException;
230            }
231
232            this.inputStream.cleanup();
233
234            // remove session from manager
235            // Thanks Google Error Prone for finding the bug where remove() was called with 'this' as argument. Changed
236            // now to remove(byteStreamRequest.getSessionID).
237            InBandBytestreamManager.getByteStreamManager(this.connection).getSessions().remove(byteStreamRequest.getSessionID());
238        }
239
240    }
241
242    /**
243     * IBBInputStream class is the base implementation of an In-Band Bytestream input stream.
244     * Subclasses of this input stream must provide a stanza(/packet) listener along with a stanza(/packet) filter to
245     * collect the In-Band Bytestream data packets.
246     */
247    private abstract class IBBInputStream extends InputStream {
248
249        /* the data packet listener to fill the data queue */
250        private final StanzaListener dataPacketListener;
251
252        /* queue containing received In-Band Bytestream data packets */
253        protected final BlockingQueue<DataPacketExtension> dataQueue = new LinkedBlockingQueue<DataPacketExtension>();
254
255        /* buffer containing the data from one data packet */
256        private byte[] buffer;
257
258        /* pointer to the next byte to read from buffer */
259        private int bufferPointer = -1;
260
261        /* data packet sequence (range from 0 to 65535) */
262        private long seq = -1;
263
264        /* flag to indicate if input stream is closed */
265        private boolean isClosed = false;
266
267        /* flag to indicate if close method was invoked */
268        private boolean closeInvoked = false;
269
270        /* timeout for read operations */
271        private int readTimeout = 0;
272
273        /**
274         * Constructor.
275         */
276        public IBBInputStream() {
277            // add data packet listener to connection
278            this.dataPacketListener = getDataPacketListener();
279            connection.addSyncStanzaListener(this.dataPacketListener, getDataPacketFilter());
280        }
281
282        /**
283         * Returns the stanza(/packet) listener that processes In-Band Bytestream data packets.
284         * 
285         * @return the data stanza(/packet) listener
286         */
287        protected abstract StanzaListener getDataPacketListener();
288
289        /**
290         * Returns the stanza(/packet) filter that accepts In-Band Bytestream data packets.
291         * 
292         * @return the data stanza(/packet) filter
293         */
294        protected abstract StanzaFilter getDataPacketFilter();
295
296        @Override
297        public synchronized int read() throws IOException {
298            checkClosed();
299
300            // if nothing read yet or whole buffer has been read fill buffer
301            if (bufferPointer == -1 || bufferPointer >= buffer.length) {
302                // if no data available and stream was closed return -1
303                if (!loadBuffer()) {
304                    return -1;
305                }
306            }
307
308            // return byte and increment buffer pointer
309            return buffer[bufferPointer++] & 0xff;
310        }
311
312        @Override
313        public synchronized int read(byte[] b, int off, int len) throws IOException {
314            if (b == null) {
315                throw new NullPointerException();
316            }
317            else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
318                            || ((off + len) < 0)) {
319                throw new IndexOutOfBoundsException();
320            }
321            else if (len == 0) {
322                return 0;
323            }
324
325            checkClosed();
326
327            // if nothing read yet or whole buffer has been read fill buffer
328            if (bufferPointer == -1 || bufferPointer >= buffer.length) {
329                // if no data available and stream was closed return -1
330                if (!loadBuffer()) {
331                    return -1;
332                }
333            }
334
335            // if more bytes wanted than available return all available
336            int bytesAvailable = buffer.length - bufferPointer;
337            if (len > bytesAvailable) {
338                len = bytesAvailable;
339            }
340
341            System.arraycopy(buffer, bufferPointer, b, off, len);
342            bufferPointer += len;
343            return len;
344        }
345
346        @Override
347        public synchronized int read(byte[] b) throws IOException {
348            return read(b, 0, b.length);
349        }
350
351        /**
352         * This method blocks until a data stanza(/packet) is received, the stream is closed or the current
353         * thread is interrupted.
354         * 
355         * @return <code>true</code> if data was received, otherwise <code>false</code>
356         * @throws IOException if data packets are out of sequence
357         */
358        private synchronized boolean loadBuffer() throws IOException {
359
360            // wait until data is available or stream is closed
361            DataPacketExtension data = null;
362            try {
363                if (this.readTimeout == 0) {
364                    while (data == null) {
365                        if (isClosed && this.dataQueue.isEmpty()) {
366                            return false;
367                        }
368                        data = this.dataQueue.poll(1000, TimeUnit.MILLISECONDS);
369                    }
370                }
371                else {
372                    data = this.dataQueue.poll(this.readTimeout, TimeUnit.MILLISECONDS);
373                    if (data == null) {
374                        throw new SocketTimeoutException();
375                    }
376                }
377            }
378            catch (InterruptedException e) {
379                // Restore the interrupted status
380                Thread.currentThread().interrupt();
381                return false;
382            }
383
384            // handle sequence overflow
385            if (this.seq == 65535) {
386                this.seq = -1;
387            }
388
389            // check if data packets sequence is successor of last seen sequence
390            long seq = data.getSeq();
391            if (seq - 1 != this.seq) {
392                // packets out of order; close stream/session
393                InBandBytestreamSession.this.close();
394                throw new IOException("Packets out of sequence");
395            }
396            else {
397                this.seq = seq;
398            }
399
400            // set buffer to decoded data
401            buffer = data.getDecodedData();
402            bufferPointer = 0;
403            return true;
404        }
405
406        /**
407         * Checks if this stream is closed and throws an IOException if necessary
408         * 
409         * @throws IOException if stream is closed and no data should be read anymore
410         */
411        private void checkClosed() throws IOException {
412            // Throw an exception if, and only if, this stream has been already
413            // closed by the user using the close() method
414            if (closeInvoked) {
415                // clear data queue in case additional data was received after stream was closed
416                this.dataQueue.clear();
417                throw new IOException("Stream is closed");
418            }
419        }
420
421        @Override
422        public boolean markSupported() {
423            return false;
424        }
425
426        @Override
427        public void close() throws IOException {
428            if (closeInvoked) {
429                return;
430            }
431
432            this.closeInvoked = true;
433
434            InBandBytestreamSession.this.closeByLocal(true);
435        }
436
437        /**
438         * This method sets the close flag and removes the data stanza(/packet) listener.
439         */
440        private void closeInternal() {
441            if (isClosed) {
442                return;
443            }
444            isClosed = true;
445        }
446
447        /**
448         * Invoked if the session is closed.
449         */
450        private void cleanup() {
451            connection.removeSyncStanzaListener(this.dataPacketListener);
452        }
453
454    }
455
456    /**
457     * IQIBBInputStream class implements IBBInputStream to be used with IQ stanzas encapsulating the
458     * data packets.
459     */
460    private class IQIBBInputStream extends IBBInputStream {
461
462        @Override
463        protected StanzaListener getDataPacketListener() {
464            return new StanzaListener() {
465
466                private long lastSequence = -1;
467
468                @Override
469                public void processStanza(Stanza packet) throws NotConnectedException, InterruptedException {
470                    // get data packet extension
471                    DataPacketExtension data = ((Data) packet).getDataPacketExtension();
472
473                    /*
474                     * check if sequence was not used already (see XEP-0047 Section 2.2)
475                     */
476                    if (data.getSeq() <= this.lastSequence) {
477                        IQ unexpectedRequest = IQ.createErrorResponse((IQ) packet,
478                                        XMPPError.Condition.unexpected_request);
479                        connection.sendStanza(unexpectedRequest);
480                        return;
481
482                    }
483
484                    // check if encoded data is valid (see XEP-0047 Section 2.2)
485                    if (data.getDecodedData() == null) {
486                        // data is invalid; respond with bad-request error
487                        IQ badRequest = IQ.createErrorResponse((IQ) packet,
488                                        XMPPError.Condition.bad_request);
489                        connection.sendStanza(badRequest);
490                        return;
491                    }
492
493                    // data is valid; add to data queue
494                    dataQueue.offer(data);
495
496                    // confirm IQ
497                    IQ confirmData = IQ.createResultIQ((IQ) packet);
498                    connection.sendStanza(confirmData);
499
500                    // set last seen sequence
501                    this.lastSequence = data.getSeq();
502                    if (this.lastSequence == 65535) {
503                        this.lastSequence = -1;
504                    }
505
506                }
507
508            };
509        }
510
511        @Override
512        protected StanzaFilter getDataPacketFilter() {
513            /*
514             * filter all IQ stanzas having type 'SET' (represented by Data class), containing a
515             * data stanza(/packet) extension, matching session ID and recipient
516             */
517            return new AndFilter(new StanzaTypeFilter(Data.class), new IBBDataPacketFilter());
518        }
519
520    }
521
522    /**
523     * MessageIBBInputStream class implements IBBInputStream to be used with message stanzas
524     * encapsulating the data packets.
525     */
526    private class MessageIBBInputStream extends IBBInputStream {
527
528        @Override
529        protected StanzaListener getDataPacketListener() {
530            return new StanzaListener() {
531
532                @Override
533                public void processStanza(Stanza packet) {
534                    // get data packet extension
535                    DataPacketExtension data = (DataPacketExtension) packet.getExtension(
536                                    DataPacketExtension.ELEMENT,
537                                    DataPacketExtension.NAMESPACE);
538
539                    // check if encoded data is valid
540                    if (data.getDecodedData() == null) {
541                        /*
542                         * TODO once a majority of XMPP server implementation support XEP-0079
543                         * Advanced Message Processing the invalid message could be answered with an
544                         * appropriate error. For now we just ignore the packet. Subsequent packets
545                         * with an increased sequence will cause the input stream to close the
546                         * stream/session.
547                         */
548                        return;
549                    }
550
551                    // data is valid; add to data queue
552                    dataQueue.offer(data);
553
554                    // TODO confirm packet once XMPP servers support XEP-0079
555                }
556
557            };
558        }
559
560        @Override
561        protected StanzaFilter getDataPacketFilter() {
562            /*
563             * filter all message stanzas containing a data stanza(/packet) extension, matching session ID
564             * and recipient
565             */
566            return new AndFilter(new StanzaTypeFilter(Message.class), new IBBDataPacketFilter());
567        }
568
569    }
570
571    /**
572     * IBBDataPacketFilter class filters all packets from the remote peer of this session,
573     * containing an In-Band Bytestream data stanza(/packet) extension whose session ID matches this sessions
574     * ID.
575     */
576    private class IBBDataPacketFilter implements StanzaFilter {
577
578        @Override
579        public boolean accept(Stanza packet) {
580            // sender equals remote peer
581            if (!packet.getFrom().equals(remoteJID)) {
582                return false;
583            }
584
585            DataPacketExtension data;
586            if (packet instanceof Data) {
587                data = ((Data) packet).getDataPacketExtension();
588            } else {
589                // stanza contains data packet extension
590                data = packet.getExtension(
591                        DataPacketExtension.ELEMENT,
592                        DataPacketExtension.NAMESPACE);
593                if (data == null) {
594                    return false;
595                }
596            }
597
598            // session ID equals this session ID
599            if (!data.getSessionID().equals(byteStreamRequest.getSessionID())) {
600                return false;
601            }
602
603            return true;
604        }
605
606    }
607
608    /**
609     * IBBOutputStream class is the base implementation of an In-Band Bytestream output stream.
610     * Subclasses of this output stream must provide a method to send data over XMPP stream.
611     */
612    private abstract class IBBOutputStream extends OutputStream {
613
614        /* buffer with the size of this sessions block size */
615        protected final byte[] buffer;
616
617        /* pointer to next byte to write to buffer */
618        protected int bufferPointer = 0;
619
620        /* data packet sequence (range from 0 to 65535) */
621        protected long seq = 0;
622
623        /* flag to indicate if output stream is closed */
624        protected boolean isClosed = false;
625
626        /**
627         * Constructor.
628         */
629        public IBBOutputStream() {
630            this.buffer = new byte[byteStreamRequest.getBlockSize()];
631        }
632
633        /**
634         * Writes the given data stanza(/packet) to the XMPP stream.
635         * 
636         * @param data the data packet
637         * @throws IOException if an I/O error occurred while sending or if the stream is closed
638         * @throws NotConnectedException 
639         * @throws InterruptedException 
640         */
641        protected abstract void writeToXML(DataPacketExtension data) throws IOException, NotConnectedException, InterruptedException;
642
643        @Override
644        public synchronized void write(int b) throws IOException {
645            if (this.isClosed) {
646                throw new IOException("Stream is closed");
647            }
648
649            // if buffer is full flush buffer
650            if (bufferPointer >= buffer.length) {
651                flushBuffer();
652            }
653
654            buffer[bufferPointer++] = (byte) b;
655        }
656
657        @Override
658        public synchronized void write(byte[] b, int off, int len) throws IOException {
659            if (b == null) {
660                throw new NullPointerException();
661            }
662            else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
663                            || ((off + len) < 0)) {
664                throw new IndexOutOfBoundsException();
665            }
666            else if (len == 0) {
667                return;
668            }
669
670            if (this.isClosed) {
671                throw new IOException("Stream is closed");
672            }
673
674            // is data to send greater than buffer size
675            if (len >= buffer.length) {
676
677                // "byte" off the first chunk to write out
678                writeOut(b, off, buffer.length);
679
680                // recursively call this method with the lesser amount
681                write(b, off + buffer.length, len - buffer.length);
682            }
683            else {
684                writeOut(b, off, len);
685            }
686        }
687
688        @Override
689        public synchronized void write(byte[] b) throws IOException {
690            write(b, 0, b.length);
691        }
692
693        /**
694         * Fills the buffer with the given data and sends it over the XMPP stream if the buffers
695         * capacity has been reached. This method is only called from this class so it is assured
696         * that the amount of data to send is <= buffer capacity
697         * 
698         * @param b the data
699         * @param off the data
700         * @param len the number of bytes to write
701         * @throws IOException if an I/O error occurred while sending or if the stream is closed
702         */
703        private synchronized void writeOut(byte[] b, int off, int len) throws IOException {
704            if (this.isClosed) {
705                throw new IOException("Stream is closed");
706            }
707
708            // set to 0 in case the next 'if' block is not executed
709            int available = 0;
710
711            // is data to send greater that buffer space left
712            if (len > buffer.length - bufferPointer) {
713                // fill buffer to capacity and send it
714                available = buffer.length - bufferPointer;
715                System.arraycopy(b, off, buffer, bufferPointer, available);
716                bufferPointer += available;
717                flushBuffer();
718            }
719
720            // copy the data left to buffer
721            System.arraycopy(b, off + available, buffer, bufferPointer, len - available);
722            bufferPointer += len - available;
723        }
724
725        @Override
726        public synchronized void flush() throws IOException {
727            if (this.isClosed) {
728                throw new IOException("Stream is closed");
729            }
730            flushBuffer();
731        }
732
733        private synchronized void flushBuffer() throws IOException {
734
735            // do nothing if no data to send available
736            if (bufferPointer == 0) {
737                return;
738            }
739
740            // create data packet
741            String enc = Base64.encodeToString(buffer, 0, bufferPointer);
742            DataPacketExtension data = new DataPacketExtension(byteStreamRequest.getSessionID(),
743                            this.seq, enc);
744
745            // write to XMPP stream
746            try {
747                writeToXML(data);
748            }
749            catch (InterruptedException | NotConnectedException e) {
750                IOException ioException = new IOException();
751                ioException.initCause(e);
752                throw ioException;
753            }
754
755            // reset buffer pointer
756            bufferPointer = 0;
757
758            // increment sequence, considering sequence overflow
759            this.seq = (this.seq + 1 == 65535 ? 0 : this.seq + 1);
760
761        }
762
763        @Override
764        public void close() throws IOException {
765            if (isClosed) {
766                return;
767            }
768            InBandBytestreamSession.this.closeByLocal(false);
769        }
770
771        /**
772         * Sets the close flag and optionally flushes the stream.
773         * 
774         * @param flush if <code>true</code> flushes the stream
775         */
776        protected void closeInternal(boolean flush) {
777            if (this.isClosed) {
778                return;
779            }
780            this.isClosed = true;
781
782            try {
783                if (flush) {
784                    flushBuffer();
785                }
786            }
787            catch (IOException e) {
788                /*
789                 * ignore, because writeToXML() will not throw an exception if stream is already
790                 * closed
791                 */
792            }
793        }
794
795    }
796
797    /**
798     * IQIBBOutputStream class implements IBBOutputStream to be used with IQ stanzas encapsulating
799     * the data packets.
800     */
801    private class IQIBBOutputStream extends IBBOutputStream {
802
803        @Override
804        protected synchronized void writeToXML(DataPacketExtension data) throws IOException {
805            // create IQ stanza containing data packet
806            IQ iq = new Data(data);
807            iq.setTo(remoteJID);
808
809            try {
810                connection.createStanzaCollectorAndSend(iq).nextResultOrThrow();
811            }
812            catch (Exception e) {
813                // close session unless it is already closed
814                if (!this.isClosed) {
815                    InBandBytestreamSession.this.close();
816                    // Sadly we are unable to use the IOException(Throwable) constructor because this
817                    // constructor is only supported from Android API 9 on.
818                    IOException ioException = new IOException();
819                    ioException.initCause(e);
820                    throw ioException;
821                }
822            }
823
824        }
825
826    }
827
828    /**
829     * MessageIBBOutputStream class implements IBBOutputStream to be used with message stanzas
830     * encapsulating the data packets.
831     */
832    private class MessageIBBOutputStream extends IBBOutputStream {
833
834        @Override
835        protected synchronized void writeToXML(DataPacketExtension data) throws NotConnectedException, InterruptedException {
836            // create message stanza containing data packet
837            Message message = new Message(remoteJID);
838            message.addExtension(data);
839
840            connection.sendStanza(message);
841
842        }
843
844    }
845
846    /**
847     * Process IQ stanza.
848     * @param data
849     * @throws NotConnectedException
850     * @throws InterruptedException 
851     * @throws NotLoggedInException 
852     */
853    public void processIQPacket(Data data) throws NotConnectedException, InterruptedException, NotLoggedInException {
854        inputStream.dataPacketListener.processStanza(data);
855    }
856
857}