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