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