/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.imagepipeline.memory;
import javax.annotation.concurrent.NotThreadSafe;
import java.io.IOException;
import java.io.InputStream;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.logging.FLog;
import com.facebook.common.references.ResourceReleaser;
/**
* InputStream that wraps another input stream and buffers all reads.
*
* <p> For purpose of buffering a byte array is used. It is provided during construction time
* together with ResourceReleaser responsible for releasing it when the stream is closed.
*/
@NotThreadSafe
public class PooledByteArrayBufferedInputStream extends InputStream {
private static final String TAG = "PooledByteInputStream";
private final InputStream mInputStream;
private final byte[] mByteArray;
private final ResourceReleaser<byte[]> mResourceReleaser;
/**
* how many bytes in mByteArray were set by last call to mInputStream.read
*/
private int mBufferedSize;
/**
* position of next buffered byte in mByteArray to be read
*
* <p> invariant: 0 <= mBufferOffset <= mBufferedSize
*/
private int mBufferOffset;
private boolean mClosed;
public PooledByteArrayBufferedInputStream(
InputStream inputStream,
byte[] byteArray,
ResourceReleaser<byte[]> resourceReleaser) {
mInputStream = Preconditions.checkNotNull(inputStream);
mByteArray = Preconditions.checkNotNull(byteArray);
mResourceReleaser = Preconditions.checkNotNull(resourceReleaser);
mBufferedSize = 0;
mBufferOffset = 0;
mClosed = false;
}
@Override
public int read() throws IOException {
Preconditions.checkState(mBufferOffset <= mBufferedSize);
ensureNotClosed();
if (!ensureDataInBuffer()) {
return -1;
}
return mByteArray[mBufferOffset++] & 0xFF;
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
Preconditions.checkState(mBufferOffset <= mBufferedSize);
ensureNotClosed();
if (!ensureDataInBuffer()) {
return -1;
}
final int bytesToRead = Math.min(mBufferedSize - mBufferOffset, length);
System.arraycopy(mByteArray, mBufferOffset, buffer, offset, bytesToRead);
mBufferOffset += bytesToRead;
return bytesToRead;
}
@Override
public int available() throws IOException {
Preconditions.checkState(mBufferOffset <= mBufferedSize);
ensureNotClosed();
return mBufferedSize - mBufferOffset + mInputStream.available();
}
@Override
public void close() throws IOException {
if (!mClosed) {
mClosed = true;
mResourceReleaser.release(mByteArray);
super.close();
}
}
@Override
public long skip(long byteCount) throws IOException {
Preconditions.checkState(mBufferOffset <= mBufferedSize);
ensureNotClosed();
final int bytesLeftInBuffer = mBufferedSize - mBufferOffset;
if (bytesLeftInBuffer >= byteCount) {
mBufferOffset += byteCount;
return byteCount;
}
mBufferOffset = mBufferedSize;
return bytesLeftInBuffer + mInputStream.skip(byteCount - bytesLeftInBuffer);
}
/**
* Checks if there is some data left in the buffer. If not but buffered stream still has some
* data to be read, then more data is buffered.
*
* @return false if and only if there is no more data and underlying input stream has no more data
* to be read
* @throws IOException
*/
private boolean ensureDataInBuffer() throws IOException {
if (mBufferOffset < mBufferedSize) {
return true;
}
final int readData = mInputStream.read(mByteArray);
if (readData <= 0) {
return false;
}
mBufferedSize = readData;
mBufferOffset = 0;
return true;
}
private void ensureNotClosed() throws IOException {
if (mClosed) {
throw new IOException("stream already closed");
}
}
@Override
protected void finalize() throws Throwable {
if (!mClosed) {
FLog.e(TAG, "Finalized without closing");
close();
}
super.finalize();
}
}