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