1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 package org.mozilla.geckoview; 6 7 import androidx.annotation.AnyThread; 8 import androidx.annotation.NonNull; 9 import java.io.IOException; 10 import java.io.InputStream; 11 import java.nio.ByteBuffer; 12 import java.util.LinkedList; 13 import org.mozilla.gecko.annotation.WrapForJNI; 14 import org.mozilla.gecko.mozglue.JNIObject; 15 import org.mozilla.gecko.util.ThreadUtils; 16 17 /** 18 * This class provides an {@link InputStream} wrapper for a Gecko nsIChannel (or really, 19 * nsIRequest). 20 */ 21 @WrapForJNI 22 @AnyThread 23 /* package */ class GeckoInputStream extends InputStream { 24 private static final String LOGTAG = "GeckoInputStream"; 25 26 private LinkedList<ByteBuffer> mBuffers = new LinkedList<>(); 27 private boolean mEOF; 28 private boolean mClosed; 29 private boolean mHaveError; 30 private long mReadTimeout; 31 private boolean mResumed; 32 private Support mSupport; 33 34 /** 35 * This is only called via JNI. The support instance provides callbacks for the native 36 * counterpart. 37 * 38 * @param support An instance of {@link Support}, used for native callbacks. 39 */ GeckoInputStream(final @NonNull Support support)40 private GeckoInputStream(final @NonNull Support support) { 41 mSupport = support; 42 } 43 setReadTimeoutMillis(final long millis)44 public void setReadTimeoutMillis(final long millis) { 45 mReadTimeout = millis; 46 } 47 48 @Override close()49 public synchronized void close() throws IOException { 50 super.close(); 51 mClosed = true; 52 53 if (mSupport != null) { 54 mSupport.close(); 55 mSupport = null; 56 } 57 } 58 59 @Override available()60 public synchronized int available() throws IOException { 61 if (mClosed) { 62 return 0; 63 } 64 65 final ByteBuffer buf = mBuffers.peekFirst(); 66 return buf != null ? buf.remaining() : 0; 67 } 68 ensureNotClosed()69 private void ensureNotClosed() throws IOException { 70 if (mClosed) { 71 throw new IOException("Stream is closed"); 72 } 73 } 74 75 @Override read()76 public synchronized int read() throws IOException { 77 ensureNotClosed(); 78 79 final int expect = Integer.SIZE / 8; 80 final byte[] bytes = new byte[expect]; 81 82 int count = 0; 83 while (count < expect) { 84 final long bytesRead = read(bytes, count, expect - count); 85 if (bytesRead < 0) { 86 return -1; 87 } 88 89 count += bytesRead; 90 } 91 92 final ByteBuffer buffer = ByteBuffer.wrap(bytes); 93 return buffer.getInt(); 94 } 95 96 @Override read(final @NonNull byte[] b)97 public int read(final @NonNull byte[] b) throws IOException { 98 return read(b, 0, b.length); 99 } 100 101 @Override read(final @NonNull byte[] dest, final int offset, final int length)102 public synchronized int read(final @NonNull byte[] dest, final int offset, final int length) 103 throws IOException { 104 ensureNotClosed(); 105 106 final long startTime = System.currentTimeMillis(); 107 while (!mEOF && mBuffers.size() == 0) { 108 if (mReadTimeout > 0 && (System.currentTimeMillis() - startTime) >= mReadTimeout) { 109 throw new IOException("Timed out"); 110 } 111 112 // The underlying channel is suspended, so resume that before 113 // waiting for a buffer. 114 if (!mResumed) { 115 mSupport.resume(); 116 mResumed = true; 117 } 118 119 try { 120 wait(mReadTimeout); 121 } catch (final InterruptedException e) { 122 } 123 } 124 125 if (mEOF && mBuffers.size() == 0) { 126 if (mHaveError) { 127 throw new IOException("Unknown error"); 128 } 129 130 // We have no data and we're not expecting more. 131 return -1; 132 } 133 134 final ByteBuffer buf = mBuffers.peekFirst(); 135 final int readCount = Math.min(length, buf.remaining()); 136 buf.get(dest, offset, readCount); 137 138 if (buf.remaining() == 0) { 139 // We're done with this buffer, advance the queue. 140 mBuffers.removeFirst(); 141 } 142 143 return readCount; 144 } 145 146 /** Called by native code to indicate that no more data will be sent via {@link #appendBuffer}. */ 147 @WrapForJNI(calledFrom = "gecko") sendEof()148 public synchronized void sendEof() { 149 if (mEOF) { 150 throw new IllegalStateException("Already have EOF"); 151 } 152 153 mEOF = true; 154 notifyAll(); 155 } 156 157 /** Called by native code to indicate that there was an error while reading the stream. */ 158 @WrapForJNI(calledFrom = "gecko") sendError()159 public synchronized void sendError() { 160 if (mEOF) { 161 throw new IllegalStateException("Already have EOF"); 162 } 163 164 mEOF = true; 165 mHaveError = true; 166 notifyAll(); 167 } 168 169 /** 170 * Called by native code to provide data for this stream. 171 * 172 * @param buf the bytes 173 * @throws IOException 174 */ 175 @WrapForJNI(exceptionMode = "nsresult", calledFrom = "gecko") appendBuffer(final byte[] buf)176 private synchronized void appendBuffer(final byte[] buf) throws IOException { 177 ThreadUtils.assertOnGeckoThread(); 178 179 if (mClosed) { 180 throw new IllegalStateException("Stream is closed"); 181 } 182 183 if (mEOF) { 184 throw new IllegalStateException("EOF, no more data expected"); 185 } 186 187 mBuffers.add(ByteBuffer.wrap(buf)); 188 notifyAll(); 189 } 190 191 @WrapForJNI 192 private static class Support extends JNIObject { 193 @WrapForJNI(dispatchTo = "gecko") resume()194 private native void resume(); 195 196 @WrapForJNI(dispatchTo = "gecko") close()197 private native void close(); 198 199 @Override // JNIObject disposeNative()200 protected void disposeNative() { 201 throw new UnsupportedOperationException(); 202 } 203 } 204 } 205