/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bitmap.util; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; /** * Wrapper for {@link InputStream} that allows you to read bytes from it like a byte[]. An * internal buffer is kept as small as possible to avoid large unnecessary allocations. * *
* Care must be taken so that the internal buffer is kept small. The best practice is to * precalculate the maximum buffer size that you will need. For example, * say you have a loop that reads bytes from index0
to 10
,
* skips to index N
, reads from index N
to N+10
, etc. Then
* you know that the internal buffer can have a maximum size of 10
,
* and you should set the bufferSize
parameter to 10
in the constructor.
*
*
* Use {@link #advanceTo(int)} to declare that you will not need to access lesser indexes. This
* helps to keep the internal buffer small. In the above example, after reading bytes from index
* 0
to 10
, you should call advanceTo(N)
so that internal
* buffer becomes filled with bytes from index N
to N+10
.
*
*
* If you know that you are reading bytes from a strictly increasing or equal
* index, then you should set the autoAdvance
parameter to true
in the
* constructor. For complicated access patterns, or when you prefer to control the internal
* buffer yourself, set autoAdvance
to false
. When
* autoAdvance
is enabled, every time an index is beyond the buffer length,
* the buffer will be shifted forward such that the index requested becomes the first element in
* the buffer.
*
*
* All public methods with parameter index
are absolute indexed. The index is from
* the beginning of the wrapped input stream.
*/
public class InputStreamBuffer {
private static final boolean DEBUG = false;
private static final int DEBUG_MAX_BUFFER_SIZE = 80;
private static final String TAG = InputStreamBuffer.class.getSimpleName();
private InputStream mInputStream;
private byte[] mBuffer;
private boolean mAutoAdvance;
/** Byte count the buffer is offset by. */
private int mOffset = 0;
/** Number of bytes filled in the buffer. */
private int mFilled = 0;
/**
* Construct a new wrapper for an InputStream.
*
*
* If autoAdvance
is true, behavior is undefined if you call {@link #get(int)}
* or {@link #has(int)} with an index N, then some arbitrary time later call {@link #get(int)}
* or {@link #has(int)} with an index M < N. The wrapper may return the right value,
* if the buffer happens to still contain index M, but more likely it will throw an
* {@link IllegalStateException}.
*
*
* If autoAdvance
is false, you must be diligent and call {@link #advanceTo(int)}
* at the appropriate times to ensure that the internal buffer is not unnecessarily resized
* and reallocated.
*
* @param inputStream The input stream to wrap. The input stream will not be closed by the
* wrapper.
* @param bufferSize The initial size for the internal buffer. The buffer size should be
* carefully chosen to avoid resizing and reallocating the internal buffer.
* The internal buffer size used will be the least power of two greater
* than this parameter.
* @param autoAdvance Determines the behavior when you need to read an index that is beyond
* the internal buffer size. If true, the internal buffer will shift so
* that the requested index becomes the first element. If false,
* the internal buffer size will grow to the smallest power of 2 which is
* greater than the requested index.
*/
public InputStreamBuffer(final InputStream inputStream, int bufferSize,
final boolean autoAdvance) {
mInputStream = inputStream;
if (bufferSize <= 0) {
throw new IllegalArgumentException(
String.format("Buffer size %d must be positive.", bufferSize));
}
bufferSize = leastPowerOf2(bufferSize);
mBuffer = new byte[bufferSize];
mAutoAdvance = autoAdvance;
}
/**
* Attempt to get byte at the requested index from the wrapped input stream. If the internal
* buffer contains the requested index, return immediately. If the index is less than the
* head of the buffer, or the index is greater or equal to the size of the wrapped input stream,
* a runtime exception is thrown.
*
*
* If the index is not in the internal buffer, but it can be requested from the input stream,
* {@link #fill(int)} will be called first, and the byte at the index returned.
*
*
* You should always call {@link #has(int)} with the same index, unless you are sure that no
* exceptions will be thrown as described above.
*
*
* Consider calling {@link #advanceTo(int)} if you know that you will never request a lesser
* index in the future.
* @param index The requested index.
* @return The byte at that index.
*/
public byte get(final int index) throws IllegalStateException, IndexOutOfBoundsException {
Trace.beginSection("get");
if (has(index)) {
final int i = index - mOffset;
Trace.endSection();
return mBuffer[i];
} else {
Trace.endSection();
throw new IndexOutOfBoundsException(
String.format("Index %d beyond length.", index));
}
}
/**
* Attempt to return whether the requested index is within the size of the wrapped input
* stream. One side effect is {@link #fill(int)} will be called.
*
*
* If this method returns true, it is guaranteed that {@link #get(int)} with the same index
* will not fail. That means that if the requested index is within the size of the wrapped
* input stream, but the index is less than the head of the internal buffer,
* a runtime exception is thrown.
*
*
* See {@link #get(int)} for caveats. A lot of the same warnings about exceptions and
* advanceTo()
apply.
* @param index The requested index.
* @return True if requested index is within the size of the wrapped input stream. False if
* the index is beyond the size.
*/
public boolean has(final int index) throws IllegalStateException, IndexOutOfBoundsException {
Trace.beginSection("has");
if (index < mOffset) {
Trace.endSection();
throw new IllegalStateException(
String.format("Index %d is before buffer %d", index, mOffset));
}
final int i = index - mOffset;
// Requested index not in internal buffer.
if (i >= mFilled || i >= mBuffer.length) {
Trace.endSection();
return fill(index);
}
Trace.endSection();
return true;
}
/**
* Attempts to advance the head of the buffer to the requested index. If the index is less
* than the head of the buffer, the internal state will not be changed.
*
*
* Advancing does not fill the internal buffer. The next {@link #get(int)} or
* {@link #has(int)} call will fill the buffer.
*/
public void advanceTo(final int index) throws IllegalStateException, IndexOutOfBoundsException {
Trace.beginSection("advance to");
final int i = index - mOffset;
if (i <= 0) {
// noop
Trace.endSection();
return;
} else if (i < mFilled) {
// Shift elements starting at i to position 0.
shiftToBeginning(i);
mOffset = index;
mFilled = mFilled - i;
} else if (mInputStream != null) {
// Burn some bytes from the input stream to match the new index.
int burn = i - mFilled;
boolean empty = false;
int fails = 0;
try {
while (burn > 0) {
final long burned = mInputStream.skip(burn);
if (burned <= 0) {
fails++;
} else {
burn -= burned;
}
if (fails >= 5) {
empty = true;
break;
}
}
} catch (IOException ignored) {
empty = true;
}
if (empty) {
//Mark input stream as consumed.
mInputStream = null;
}
mOffset = index - burn;
mFilled = 0;
} else {
// Advancing beyond the input stream.
mOffset = index;
mFilled = 0;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("advanceTo %d buffer: %s", i, this));
}
Trace.endSection();
}
/**
* Attempt to fill the internal buffer fully. The buffer will be modified such that the
* requested index will always be in the buffer. If the index is less
* than the head of the buffer, a runtime exception is thrown.
*
*
* If the requested index is already in bounds of the buffer, then the buffer will just be
* filled.
*
*
* Otherwise, if autoAdvance
was set to true in the constructor,
* {@link #advanceTo(int)} will be called with the requested index,
* and then the buffer filled. If autoAdvance
was set to false,
* we allocate a single larger buffer of a least multiple-of-two size that can contain the
* requested index. The elements in the old buffer are copied over to the head of the new
* buffer. Then the entire buffer is filled.
* @param index The requested index.
* @return True if the byte at the requested index has been filled. False if the wrapped
* input stream ends before we reach the index.
*/
private boolean fill(final int index) {
Trace.beginSection("fill");
if (index < mOffset) {
Trace.endSection();
throw new IllegalStateException(
String.format("Index %d is before buffer %d", index, mOffset));
}
int i = index - mOffset;
// Can't fill buffer anymore if input stream is consumed.
if (mInputStream == null) {
Trace.endSection();
return false;
}
// Increase buffer size if necessary.
int length = i + 1;
if (length > mBuffer.length) {
if (mAutoAdvance) {
advanceTo(index);
i = index - mOffset;
} else {
length = leastPowerOf2(length);
Log.w(TAG, String.format(
"Increasing buffer length from %d to %d. Bad buffer size chosen, "
+ "or advanceTo() not called.",
mBuffer.length, length));
mBuffer = Arrays.copyOf(mBuffer, length);
}
}
// Read from input stream to fill buffer.
int read = -1;
try {
read = mInputStream.read(mBuffer, mFilled, mBuffer.length - mFilled);
} catch (IOException ignored) {
}
if (read != -1) {
mFilled = mFilled + read;
} else {
// Mark input stream as consumed.
mInputStream = null;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("fill %d buffer: %s", i, this));
}
Trace.endSection();
return i < mFilled;
}
/**
* Modify the internal buffer so that all the bytes are shifted towards the head by
* i
. In other words, the byte at index i
will now be at index
* 0
. Bytes from a lesser index are tossed.
* @param i How much to shift left.
*/
private void shiftToBeginning(final int i) {
if (i >= mBuffer.length) {
throw new IndexOutOfBoundsException(
String.format("Index %d out of bounds. Length %d", i, mBuffer.length));
}
for (int j = 0; j + i < mFilled; j++) {
mBuffer[j] = mBuffer[j + i];
}
}
@Override
public String toString() {
if (DEBUG) {
return toDebugString();
}
return String.format("+%d+%d [%d]", mOffset, mBuffer.length, mFilled);
}
public String toDebugString() {
Trace.beginSection("to debug string");
final StringBuilder sb = new StringBuilder();
sb.append("+").append(mOffset);
sb.append("+").append(mBuffer.length);
sb.append(" [");
for (int i = 0; i < mBuffer.length && i < DEBUG_MAX_BUFFER_SIZE; i++) {
if (i > 0) {
sb.append(",");
}
if (i < mFilled) {
sb.append(String.format("%02X", mBuffer[i]));
} else {
sb.append("__");
}
}
if (mInputStream != null) {
sb.append("...");
}
sb.append("]");
Trace.endSection();
return sb.toString();
}
/**
* Calculate the least power of two greater than or equal to the input.
*/
private static int leastPowerOf2(int n) {
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
n++;
return n;
}
}