1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.settings.util;
18 
19 import android.support.annotation.NonNull;
20 
21 import java.io.FilterInputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.util.ArrayList;
25 import java.util.List;
26 
27 /**
28  * A replacement of BufferedInputStream (no multiple thread): <p>
29  * - use list of byte array (chunks) instead of keep growing a single byte array (more efficent)
30  *   <br>
31  * - support overriding the markLimit passed in mark() call (The value that BitmapFactory
32  *   uses 1024 is too small for detecting bitmap bounds and reset()) <br>
33  */
34 public class CachedInputStream extends FilterInputStream {
35 
36     private static final int CHUNK_SIZE = ByteArrayPool.CHUNK16K;
37 
38     private final ArrayList<byte[]> mBufs = new ArrayList<>();
39     private int mPos = 0;  // current read position inside the chunk buffers
40     private int mCount = 0; // total validate bytes in chunk buffers
41     private int mMarkPos = -1; // marked read position in chunk buffers
42     private int mOverrideMarkLimit; // to override readlimit of mark() call
43     private int mMarkLimit; // effective marklimit
44     private final byte[] tmp = new byte[1]; // tmp buffer used in read()
45 
CachedInputStream(InputStream in)46     public CachedInputStream(InputStream in) {
47         super(in);
48     }
49 
50     @Override
markSupported()51     public boolean markSupported() {
52         return true;
53     }
54 
55     /**
56      * set the value that will override small readlimit passed in mark() call.
57      */
setOverrideMarkLimit(int overrideMarkLimit)58     public void setOverrideMarkLimit(int overrideMarkLimit) {
59         mOverrideMarkLimit = overrideMarkLimit;
60     }
61 
getOverrideMarkLimit()62     public int getOverrideMarkLimit() {
63         return mOverrideMarkLimit;
64     }
65 
66     @Override
mark(int readlimit)67     public void mark(int readlimit) {
68         readlimit = readlimit < mOverrideMarkLimit ? mOverrideMarkLimit : readlimit;
69         if (mMarkPos >= 0) {
70             // for replacing existing mark(), discard anything before mPos
71             // and move mMarkPos to mPos
72             int chunks = mPos / CHUNK_SIZE;
73             if (chunks > 0) {
74                 // trim the header buffers
75                 int removedBytes = chunks * CHUNK_SIZE;
76                 List<byte[]> subList = mBufs.subList(0, chunks);
77                 releaseChunks(subList);
78                 subList.clear();
79                 mPos = mPos - removedBytes;
80                 mCount = mCount - removedBytes;
81             }
82         }
83         mMarkPos = mPos;
84         mMarkLimit = readlimit;
85     }
86 
87     @Override
reset()88     public void reset() throws IOException {
89         if (mMarkPos < 0) {
90             throw new IOException("mark has been invalidated");
91         }
92         mPos = mMarkPos;
93     }
94 
95     @Override
read()96     public int read() throws IOException {
97         // TODO, not efficient, but the function is not called by BitmapFactory
98         int r = read(tmp, 0, 1);
99         if (r <= 0) {
100             return -1;
101         }
102         return tmp[0] & 0xFF;
103     }
104 
105     @Override
close()106     public void close() throws IOException {
107         if (in!=null) {
108             in.close();
109             in = null;
110         }
111         releaseChunks(mBufs);
112     }
113 
releaseChunks(List<byte[]> bufs)114     private static void releaseChunks(List<byte[]> bufs) {
115         ByteArrayPool.get16KBPool().releaseChunks(bufs);
116     }
117 
allocateChunk()118     private byte[] allocateChunk() {
119         return ByteArrayPool.get16KBPool().allocateChunk();
120     }
121 
invalidate()122     private boolean invalidate() {
123         if (mCount - mMarkPos > mMarkLimit) {
124             mMarkPos = -1;
125             mCount = 0;
126             mPos = 0;
127             releaseChunks(mBufs);
128             mBufs.clear();
129             return true;
130         }
131         return false;
132     }
133 
134     @Override
read(@onNull byte[] buffer, int offset, int count)135     public int read(@NonNull byte[] buffer, int offset, int count) throws IOException {
136         if (in == null) {
137             throw streamClosed();
138         }
139         if (mMarkPos == -1) {
140             int reads = in.read(buffer, offset, count);
141             return reads;
142         }
143         if (count == 0) {
144             return 0;
145         }
146         int copied = copyMarkedBuffer(buffer, offset, count);
147         count -= copied;
148         offset += copied;
149         int totalReads = copied;
150         while (count > 0) {
151             if (mPos == mBufs.size() * CHUNK_SIZE) {
152                 mBufs.add(allocateChunk());
153             }
154             int currentBuf = mPos / CHUNK_SIZE;
155             int indexInBuf = mPos - currentBuf * CHUNK_SIZE;
156             byte[] buf = mBufs.get(currentBuf);
157             int end = (currentBuf + 1) * CHUNK_SIZE;
158             int leftInBuffer = end - mPos;
159             int toRead = count > leftInBuffer ? leftInBuffer : count;
160             int reads = in.read(buf, indexInBuf, toRead);
161             if (reads > 0) {
162                 System.arraycopy(buf, indexInBuf, buffer, offset, reads);
163                 mPos += reads;
164                 mCount += reads;
165                 totalReads += reads;
166                 offset += reads;
167                 count -= reads;
168                 if (invalidate()) {
169                     reads = in.read(buffer, offset, count);
170                     if (reads >0 ) {
171                         totalReads += reads;
172                     }
173                     break;
174                 }
175             } else {
176                 break;
177             }
178         }
179         if (totalReads == 0) {
180             return -1;
181         }
182         return totalReads;
183     }
184 
copyMarkedBuffer(byte[] buffer, int offset, int read)185     private int copyMarkedBuffer(byte[] buffer, int offset, int read) {
186         int totalRead = 0;
187         while (read > 0 && mPos < mCount) {
188             int currentBuf = mPos / CHUNK_SIZE;
189             int indexInBuf = mPos - currentBuf * CHUNK_SIZE;
190             byte[] buf = mBufs.get(currentBuf);
191             int end = (currentBuf + 1) * CHUNK_SIZE;
192             if (end > mCount) {
193                 end = mCount;
194             }
195             int leftInBuffer = end - mPos;
196             int toRead = read > leftInBuffer ? leftInBuffer : read;
197             System.arraycopy(buf, indexInBuf, buffer, offset, toRead);
198             offset += toRead;
199             read -= toRead;
200             totalRead += toRead;
201             mPos += toRead;
202         }
203         return totalRead;
204     }
205 
206     @Override
available()207     public int available() throws IOException {
208         if (in == null) {
209             throw streamClosed();
210         }
211         return mCount - mPos + in.available();
212     }
213 
214     @Override
skip(long byteCount)215     public long skip(long byteCount) throws IOException {
216         if (in == null) {
217             throw streamClosed();
218         }
219         if (mMarkPos <0) {
220             return in.skip(byteCount);
221         }
222         long totalSkip = 0;
223         totalSkip = mCount - mPos;
224         if (totalSkip > byteCount) {
225             totalSkip = byteCount;
226         }
227         mPos += totalSkip;
228         byteCount -= totalSkip;
229         while (byteCount > 0) {
230             if (mPos == mBufs.size() * CHUNK_SIZE) {
231                 mBufs.add(allocateChunk());
232             }
233             int currentBuf = mPos / CHUNK_SIZE;
234             int indexInBuf = mPos - currentBuf * CHUNK_SIZE;
235             byte[] buf = mBufs.get(currentBuf);
236             int end = (currentBuf + 1) * CHUNK_SIZE;
237             int leftInBuffer = end - mPos;
238             int toRead = (int) (byteCount > leftInBuffer ? leftInBuffer : byteCount);
239             int reads = in.read(buf, indexInBuf, toRead);
240             if (reads > 0) {
241                 mPos += reads;
242                 mCount += reads;
243                 byteCount -= reads;
244                 totalSkip += reads;
245                 if (invalidate()) {
246                     if (byteCount > 0) {
247                         totalSkip += in.skip(byteCount);
248                     }
249                     break;
250                 }
251             } else {
252                 break;
253             }
254         }
255         return totalSkip;
256     }
257 
streamClosed()258     private static IOException streamClosed() {
259         return new IOException("stream closed");
260     }
261 
262 }
263