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}