Add async BufferedFile and StreamingPacketFile impls

Bug: 245971639

Change-Id: Ief1719262c2cb68819f6feb955e97793f3638ec0
This commit is contained in:
Igor Chernyshev 2023-04-06 15:52:26 -07:00
commit 810be2b456
13 changed files with 1471 additions and 2 deletions

View file

@ -328,6 +328,31 @@ java_library {
lint: { strict_updatability_linting: true },
}
java_library {
name: "net-utils-device-common-wear",
srcs: [
"device/com/android/net/module/util/wear/*.java",
],
sdk_version: "module_current",
min_sdk_version: "29",
visibility: [
"//frameworks/libs/net/common/tests:__subpackages__",
"//frameworks/libs/net/common/testutils:__subpackages__",
"//packages/modules/Connectivity:__subpackages__",
],
libs: [
"framework-annotations-lib",
],
static_libs: [
"net-utils-device-common-async",
],
apex_available: [
"com.android.tethering",
"//apex_available:platform",
],
lint: { strict_updatability_linting: true },
}
// Limited set of utilities for use by service-connectivity-mdns-standalone-build-test, to make sure
// the mDNS code can build with only system APIs.
// The mDNS code is platform code so it should use framework-annotations-lib, contrary to apps that

View file

@ -0,0 +1,292 @@
/*
* Copyright (C) 2023 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.net.module.util.async;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;
/**
* Buffers inbound and outbound file data within given strict limits.
*
* Automatically manages all readability and writeability events in EventManager:
* - When read buffer has more space - asks EventManager to notify on more data
* - When write buffer has more space - asks the user to provide more data
* - When underlying file cannot accept more data - registers EventManager callback
*
* @hide
*/
public final class BufferedFile implements AsyncFile.Listener {
/**
* Receives notifications when new data or output space is available.
* @hide
*/
public interface Listener {
/** Invoked after the underlying file has been closed. */
void onBufferedFileClosed();
/** Invoked when there's new data in the inbound buffer. */
void onBufferedFileInboundData(int readByteCount);
/** Notifies on data being flushed from output buffer. */
void onBufferedFileOutboundSpace();
/** Notifies on unrecoverable error in file access. */
void onBufferedFileIoError(String message);
}
private final Listener mListener;
private final EventManager mEventManager;
private AsyncFile mFile;
private final CircularByteBuffer mInboundBuffer;
private final AtomicLong mTotalBytesRead = new AtomicLong();
private boolean mIsReadingShutdown;
private final CircularByteBuffer mOutboundBuffer;
private final AtomicLong mTotalBytesWritten = new AtomicLong();
/** Creates BufferedFile based on the given file descriptor. */
public static BufferedFile create(
EventManager eventManager,
FileHandle fileHandle,
Listener listener,
int inboundBufferSize,
int outboundBufferSize) throws IOException {
if (fileHandle == null) {
throw new NullPointerException();
}
BufferedFile file = new BufferedFile(
eventManager, listener, inboundBufferSize, outboundBufferSize);
file.mFile = eventManager.registerFile(fileHandle, file);
return file;
}
private BufferedFile(
EventManager eventManager,
Listener listener,
int inboundBufferSize,
int outboundBufferSize) {
if (eventManager == null || listener == null) {
throw new NullPointerException();
}
mEventManager = eventManager;
mListener = listener;
mInboundBuffer = new CircularByteBuffer(inboundBufferSize);
mOutboundBuffer = new CircularByteBuffer(outboundBufferSize);
}
/** Requests this file to be closed. */
public void close() {
mFile.close();
}
@Override
public void onClosed(AsyncFile file) {
mListener.onBufferedFileClosed();
}
///////////////////////////////////////////////////////////////////////////
// READ PATH
///////////////////////////////////////////////////////////////////////////
/** Returns buffer that is automatically filled with inbound data. */
public ReadableByteBuffer getInboundBuffer() {
return mInboundBuffer;
}
public int getInboundBufferFreeSizeForTest() {
return mInboundBuffer.freeSize();
}
/** Permanently disables reading of this file, and clears all buffered data. */
public void shutdownReading() {
mIsReadingShutdown = true;
mInboundBuffer.clear();
mFile.enableReadEvents(false);
}
/** Returns true after shutdownReading() has been called. */
public boolean isReadingShutdown() {
return mIsReadingShutdown;
}
/** Starts or resumes async read operations on this file. */
public void continueReading() {
if (!mIsReadingShutdown && mInboundBuffer.freeSize() > 0) {
mFile.enableReadEvents(true);
}
}
@Override
public void onReadReady(AsyncFile file) {
if (mIsReadingShutdown) {
return;
}
int readByteCount;
try {
readByteCount = bufferInputData();
} catch (IOException e) {
mListener.onBufferedFileIoError("IOException while reading: " + e.toString());
return;
}
if (readByteCount > 0) {
mListener.onBufferedFileInboundData(readByteCount);
}
continueReading();
}
private int bufferInputData() throws IOException {
int totalReadCount = 0;
while (true) {
final int maxReadCount = mInboundBuffer.getDirectWriteSize();
if (maxReadCount == 0) {
mFile.enableReadEvents(false);
break;
}
final int bufferOffset = mInboundBuffer.getDirectWritePos();
final byte[] buffer = mInboundBuffer.getDirectWriteBuffer();
final int readCount = mFile.read(buffer, bufferOffset, maxReadCount);
if (readCount <= 0) {
break;
}
mInboundBuffer.accountForDirectWrite(readCount);
totalReadCount += readCount;
}
mTotalBytesRead.addAndGet(totalReadCount);
return totalReadCount;
}
///////////////////////////////////////////////////////////////////////////
// WRITE PATH
///////////////////////////////////////////////////////////////////////////
/** Returns the number of bytes currently buffered for output. */
public int getOutboundBufferSize() {
return mOutboundBuffer.size();
}
/** Returns the number of bytes currently available for buffering for output. */
public int getOutboundBufferFreeSize() {
return mOutboundBuffer.freeSize();
}
/**
* Queues the given data for output.
* Throws runtime exception if there is not enough space.
*/
public boolean enqueueOutboundData(byte[] data, int pos, int len) {
return enqueueOutboundData(data, pos, len, null, 0, 0);
}
/**
* Queues data1, then data2 for output.
* Throws runtime exception if there is not enough space.
*/
public boolean enqueueOutboundData(
byte[] data1, int pos1, int len1,
byte[] buffer2, int pos2, int len2) {
Assertions.throwsIfOutOfBounds(data1, pos1, len1);
Assertions.throwsIfOutOfBounds(buffer2, pos2, len2);
final int totalLen = len1 + len2;
if (totalLen > mOutboundBuffer.freeSize()) {
flushOutboundBuffer();
if (totalLen > mOutboundBuffer.freeSize()) {
return false;
}
}
mOutboundBuffer.writeBytes(data1, pos1, len1);
if (buffer2 != null) {
mOutboundBuffer.writeBytes(buffer2, pos2, len2);
}
flushOutboundBuffer();
return true;
}
private void flushOutboundBuffer() {
try {
while (mOutboundBuffer.getDirectReadSize() > 0) {
final int maxReadSize = mOutboundBuffer.getDirectReadSize();
final int writeCount = mFile.write(
mOutboundBuffer.getDirectReadBuffer(),
mOutboundBuffer.getDirectReadPos(),
maxReadSize);
if (writeCount == 0) {
mFile.enableWriteEvents(true);
break;
}
if (writeCount > maxReadSize) {
throw new IllegalArgumentException(
"Write count " + writeCount + " above max " + maxReadSize);
}
mOutboundBuffer.accountForDirectRead(writeCount);
}
} catch (IOException e) {
scheduleOnIoError("IOException while writing: " + e.toString());
}
}
private void scheduleOnIoError(String message) {
mEventManager.execute(() -> {
mListener.onBufferedFileIoError(message);
});
}
@Override
public void onWriteReady(AsyncFile file) {
mFile.enableWriteEvents(false);
flushOutboundBuffer();
mListener.onBufferedFileOutboundSpace();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("file={");
sb.append(mFile);
sb.append("}");
if (mIsReadingShutdown) {
sb.append(", readingShutdown");
}
sb.append("}, inboundBuffer={");
sb.append(mInboundBuffer);
sb.append("}, outboundBuffer={");
sb.append(mOutboundBuffer);
sb.append("}, totalBytesRead=");
sb.append(mTotalBytesRead);
sb.append(", totalBytesWritten=");
sb.append(mTotalBytesWritten);
return sb.toString();
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 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.net.module.util.wear;
import com.android.net.module.util.async.ReadableByteBuffer;
/**
* Implements utilities for decoding parts of TCP/UDP/IP headers.
*
* @hide
*/
final class NetPacketHelpers {
static void encodeNetworkUnsignedInt16(int value, byte[] dst, final int dstPos) {
dst[dstPos] = (byte) ((value >> 8) & 0xFF);
dst[dstPos + 1] = (byte) (value & 0xFF);
}
static int decodeNetworkUnsignedInt16(byte[] data, final int pos) {
return ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
}
static int decodeNetworkUnsignedInt16(ReadableByteBuffer data, final int pos) {
return ((data.peek(pos) & 0xFF) << 8) | (data.peek(pos + 1) & 0xFF);
}
private NetPacketHelpers() {}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (C) 2023 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.net.module.util.wear;
/**
* Defines bidirectional file where all transmissions are made as complete packets.
*
* Automatically manages all readability and writeability events in EventManager:
* - When read buffer has more space - asks EventManager to notify on more data
* - When write buffer has more space - asks the user to provide more data
* - When underlying file cannot accept more data - registers EventManager callback
*
* @hide
*/
public interface PacketFile {
/** @hide */
public enum ErrorCode {
UNEXPECTED_ERROR,
IO_ERROR,
INBOUND_PACKET_TOO_LARGE,
OUTBOUND_PACKET_TOO_LARGE,
}
/**
* Receives notifications when new data or output space is available.
*
* @hide
*/
public interface Listener {
/**
* Handles the initial part of the stream, which on some systems provides lower-level
* configuration data.
*
* Returns the number of bytes consumed, or zero if the preamble has been fully read.
*/
int onPreambleData(byte[] data, int pos, int len);
/** Handles one extracted packet. */
void onInboundPacket(byte[] data, int pos, int len);
/** Notifies on new data being added to the buffer. */
void onInboundBuffered(int newByteCount, int totalBufferedSize);
/** Notifies on data being flushed from output buffer. */
void onOutboundPacketSpace();
/** Notifies on unrecoverable error in the packet processing. */
void onPacketFileError(ErrorCode error, String message);
}
/** Requests this file to be closed. */
void close();
/** Permanently disables reading of this file, and clears all buffered data. */
void shutdownReading();
/** Starts or resumes async read operations on this file. */
void continueReading();
/** Returns the number of bytes currently buffered as input. */
int getInboundBufferSize();
/** Returns the number of bytes currently available for buffering for output. */
int getOutboundFreeSize();
/**
* Queues the given data for output.
* Throws runtime exception if there is not enough space.
*/
boolean enqueueOutboundPacket(byte[] data, int pos, int len);
}

View file

@ -0,0 +1,221 @@
/*
* Copyright (C) 2023 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.net.module.util.wear;
import com.android.net.module.util.async.BufferedFile;
import com.android.net.module.util.async.EventManager;
import com.android.net.module.util.async.FileHandle;
import com.android.net.module.util.async.Assertions;
import com.android.net.module.util.async.ReadableByteBuffer;
import java.io.IOException;
/**
* Implements PacketFile based on a streaming file descriptor.
*
* Packets are delineated using network-order 2-byte length indicators.
*
* @hide
*/
public final class StreamingPacketFile implements PacketFile, BufferedFile.Listener {
private static final int HEADER_SIZE = 2;
private final EventManager mEventManager;
private final Listener mListener;
private final BufferedFile mFile;
private final int mMaxPacketSize;
private final ReadableByteBuffer mInboundBuffer;
private boolean mIsInPreamble = true;
private final byte[] mTempPacketReadBuffer;
private final byte[] mTempHeaderWriteBuffer;
public StreamingPacketFile(
EventManager eventManager,
FileHandle fileHandle,
Listener listener,
int maxPacketSize,
int maxBufferedInboundPackets,
int maxBufferedOutboundPackets) throws IOException {
if (eventManager == null || fileHandle == null || listener == null) {
throw new NullPointerException();
}
mEventManager = eventManager;
mListener = listener;
mMaxPacketSize = maxPacketSize;
final int maxTotalLength = HEADER_SIZE + maxPacketSize;
mFile = BufferedFile.create(eventManager, fileHandle, this,
maxTotalLength * maxBufferedInboundPackets,
maxTotalLength * maxBufferedOutboundPackets);
mInboundBuffer = mFile.getInboundBuffer();
mTempPacketReadBuffer = new byte[maxTotalLength];
mTempHeaderWriteBuffer = new byte[HEADER_SIZE];
}
@Override
public void close() {
mFile.close();
}
public BufferedFile getUnderlyingFileForTest() {
return mFile;
}
@Override
public void shutdownReading() {
mFile.shutdownReading();
}
@Override
public void continueReading() {
mFile.continueReading();
}
@Override
public int getInboundBufferSize() {
return mInboundBuffer.size();
}
@Override
public void onBufferedFileClosed() {
}
@Override
public void onBufferedFileInboundData(int readByteCount) {
if (mFile.isReadingShutdown()) {
return;
}
if (readByteCount > 0) {
mListener.onInboundBuffered(readByteCount, mInboundBuffer.size());
}
if (extractOnePacket() && !mFile.isReadingShutdown()) {
// There could be more packets already buffered, continue parsing next
// packet even before another read event comes
mEventManager.execute(() -> {
onBufferedFileInboundData(0);
});
} else {
continueReading();
}
}
private boolean extractOnePacket() {
while (mIsInPreamble) {
final int directReadSize = Math.min(
mInboundBuffer.getDirectReadSize(), mTempPacketReadBuffer.length);
if (directReadSize == 0) {
return false;
}
// Copy for safety, so higher-level callback cannot modify the data.
System.arraycopy(mInboundBuffer.getDirectReadBuffer(),
mInboundBuffer.getDirectReadPos(), mTempPacketReadBuffer, 0, directReadSize);
final int preambleConsumedBytes = mListener.onPreambleData(
mTempPacketReadBuffer, 0, directReadSize);
if (mFile.isReadingShutdown()) {
return false; // The callback has called shutdownReading().
}
if (preambleConsumedBytes == 0) {
mIsInPreamble = false;
break;
}
mInboundBuffer.accountForDirectRead(preambleConsumedBytes);
}
final int bufferedSize = mInboundBuffer.size();
if (bufferedSize < HEADER_SIZE) {
return false;
}
final int dataLength = NetPacketHelpers.decodeNetworkUnsignedInt16(mInboundBuffer, 0);
if (dataLength > mMaxPacketSize) {
mListener.onPacketFileError(
PacketFile.ErrorCode.INBOUND_PACKET_TOO_LARGE,
"Inbound packet length: " + dataLength);
return false;
}
final int totalLength = HEADER_SIZE + dataLength;
if (bufferedSize < totalLength) {
return false;
}
mInboundBuffer.readBytes(mTempPacketReadBuffer, 0, totalLength);
mListener.onInboundPacket(mTempPacketReadBuffer, HEADER_SIZE, dataLength);
return true;
}
@Override
public int getOutboundFreeSize() {
final int freeSize = mFile.getOutboundBufferFreeSize();
return (freeSize > HEADER_SIZE ? freeSize - HEADER_SIZE : 0);
}
@Override
public boolean enqueueOutboundPacket(byte[] buffer, int pos, int len) {
Assertions.throwsIfOutOfBounds(buffer, pos, len);
if (len == 0) {
return true;
}
if (len > mMaxPacketSize) {
mListener.onPacketFileError(
PacketFile.ErrorCode.OUTBOUND_PACKET_TOO_LARGE,
"Outbound packet length: " + len);
return false;
}
NetPacketHelpers.encodeNetworkUnsignedInt16(len, mTempHeaderWriteBuffer, 0);
mFile.enqueueOutboundData(
mTempHeaderWriteBuffer, 0, mTempHeaderWriteBuffer.length,
buffer, pos, len);
return true;
}
@Override
public void onBufferedFileOutboundSpace() {
mListener.onOutboundPacketSpace();
}
@Override
public void onBufferedFileIoError(String message) {
mListener.onPacketFileError(PacketFile.ErrorCode.IO_ERROR, message);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("maxPacket=");
sb.append(mMaxPacketSize);
sb.append(", file={");
sb.append(mFile);
sb.append("}");
return sb.toString();
}
}

View file

@ -21,6 +21,7 @@ android_library {
"net-utils-device-common-async",
"net-utils-device-common-bpf",
"net-utils-device-common-ip",
"net-utils-device-common-wear",
],
libs: [
"android.test.runner",

View file

@ -0,0 +1,376 @@
/*
* Copyright (C) 2023 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.net.module.util.async;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.ignoreStubs;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.os.ParcelFileDescriptor;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.testutils.async.ReadableDataAnswer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class BufferedFileTest {
@Mock EventManager mockEventManager;
@Mock BufferedFile.Listener mockFileListener;
@Mock AsyncFile mockAsyncFile;
@Mock ParcelFileDescriptor mockParcelFileDescriptor;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
@After
public void tearDown() throws Exception {
verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager));
}
@Test
public void onClosed() throws Exception {
final int inboundBufferSize = 1024;
final int outboundBufferSize = 768;
final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
file.onClosed(mockAsyncFile);
verify(mockFileListener).onBufferedFileClosed();
}
@Test
public void continueReadingAndClose() throws Exception {
final int inboundBufferSize = 1024;
final int outboundBufferSize = 768;
final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
assertEquals(inboundBufferSize, file.getInboundBufferFreeSizeForTest());
assertEquals(outboundBufferSize, file.getOutboundBufferFreeSize());
file.continueReading();
verify(mockAsyncFile).enableReadEvents(true);
file.close();
verify(mockAsyncFile).close();
}
@Test
public void enqueueOutboundData() throws Exception {
final int inboundBufferSize = 10;
final int outboundBufferSize = 250;
final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
final byte[] data1 = new byte[101];
final byte[] data2 = new byte[102];
data1[0] = (byte) 1;
data2[0] = (byte) 2;
assertEquals(0, file.getOutboundBufferSize());
final int totalLen = data1.length + data2.length;
when(mockAsyncFile.write(any(), anyInt(), anyInt())).thenReturn(0);
assertTrue(file.enqueueOutboundData(data1, 0, data1.length, null, 0, 0));
verify(mockAsyncFile).enableWriteEvents(true);
assertEquals(data1.length, file.getOutboundBufferSize());
checkAndResetMocks();
final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
when(mockAsyncFile.write(
arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
assertTrue(file.enqueueOutboundData(data2, 0, data2.length, null, 0, 0));
assertEquals(0, file.getInboundBuffer().size());
assertEquals(0, file.getOutboundBufferSize());
assertEquals(0, posCaptor.getValue().intValue());
assertEquals(totalLen, lenCaptor.getValue().intValue());
assertEquals(data1[0], arrayCaptor.getValue()[0]);
assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
}
@Test
public void enqueueOutboundData_combined() throws Exception {
final int inboundBufferSize = 10;
final int outboundBufferSize = 250;
final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
final byte[] data1 = new byte[101];
final byte[] data2 = new byte[102];
data1[0] = (byte) 1;
data2[0] = (byte) 2;
assertEquals(0, file.getOutboundBufferSize());
final int totalLen = data1.length + data2.length;
final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
when(mockAsyncFile.write(
arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
assertTrue(file.enqueueOutboundData(data1, 0, data1.length, data2, 0, data2.length));
assertEquals(0, file.getInboundBuffer().size());
assertEquals(0, file.getOutboundBufferSize());
assertEquals(0, posCaptor.getValue().intValue());
assertEquals(totalLen, lenCaptor.getValue().intValue());
assertEquals(data1[0], arrayCaptor.getValue()[0]);
assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
}
@Test
public void enableWriteEvents() throws Exception {
final int inboundBufferSize = 10;
final int outboundBufferSize = 250;
final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
final byte[] data1 = new byte[101];
final byte[] data2 = new byte[102];
final byte[] data3 = new byte[103];
data1[0] = (byte) 1;
data2[0] = (byte) 2;
data3[0] = (byte) 3;
assertEquals(0, file.getOutboundBufferSize());
// Write first 2 buffers, but fail to flush them, causing async write request.
final int data1And2Len = data1.length + data2.length;
when(mockAsyncFile.write(any(), eq(0), eq(data1And2Len))).thenReturn(0);
assertTrue(file.enqueueOutboundData(data1, 0, data1.length, data2, 0, data2.length));
assertEquals(0, file.getInboundBuffer().size());
assertEquals(data1And2Len, file.getOutboundBufferSize());
verify(mockAsyncFile).enableWriteEvents(true);
// Try to write 3rd buffers, which won't fit, then fail to flush.
when(mockAsyncFile.write(any(), eq(0), eq(data1And2Len))).thenReturn(0);
assertFalse(file.enqueueOutboundData(data3, 0, data3.length, null, 0, 0));
assertEquals(0, file.getInboundBuffer().size());
assertEquals(data1And2Len, file.getOutboundBufferSize());
verify(mockAsyncFile, times(2)).enableWriteEvents(true);
checkAndResetMocks();
// Simulate writeability event, and successfully flush.
final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
when(mockAsyncFile.write(arrayCaptor.capture(),
posCaptor.capture(), lenCaptor.capture())).thenReturn(data1And2Len);
file.onWriteReady(mockAsyncFile);
verify(mockAsyncFile).enableWriteEvents(false);
verify(mockFileListener).onBufferedFileOutboundSpace();
assertEquals(0, file.getOutboundBufferSize());
assertEquals(0, posCaptor.getValue().intValue());
assertEquals(data1And2Len, lenCaptor.getValue().intValue());
assertEquals(data1[0], arrayCaptor.getValue()[0]);
assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
checkAndResetMocks();
// Now write, but fail to flush the third buffer.
when(mockAsyncFile.write(arrayCaptor.capture(),
posCaptor.capture(), lenCaptor.capture())).thenReturn(0);
assertTrue(file.enqueueOutboundData(data3, 0, data3.length, null, 0, 0));
verify(mockAsyncFile).enableWriteEvents(true);
assertEquals(data3.length, file.getOutboundBufferSize());
assertEquals(data1And2Len, posCaptor.getValue().intValue());
assertEquals(outboundBufferSize - data1And2Len, lenCaptor.getValue().intValue());
assertEquals(data3[0], arrayCaptor.getValue()[data1And2Len]);
}
@Test
public void read() throws Exception {
final int inboundBufferSize = 250;
final int outboundBufferSize = 10;
final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
final byte[] data1 = new byte[101];
final byte[] data2 = new byte[102];
data1[0] = (byte) 1;
data2[0] = (byte) 2;
final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data1, data2);
final ReadableByteBuffer inboundBuffer = file.getInboundBuffer();
when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
file.onReadReady(mockAsyncFile);
verify(mockAsyncFile).enableReadEvents(true);
verify(mockFileListener).onBufferedFileInboundData(eq(data1.length + data2.length));
assertEquals(0, file.getOutboundBufferSize());
assertEquals(data1.length + data2.length, inboundBuffer.size());
assertEquals((byte) 1, inboundBuffer.peek(0));
assertEquals((byte) 2, inboundBuffer.peek(data1.length));
}
@Test
public void enableReadEvents() throws Exception {
final int inboundBufferSize = 250;
final int outboundBufferSize = 10;
final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
final byte[] data1 = new byte[101];
final byte[] data2 = new byte[102];
final byte[] data3 = new byte[103];
data1[0] = (byte) 1;
data2[0] = (byte) 2;
data3[0] = (byte) 3;
final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data1, data2, data3);
final ReadableByteBuffer inboundBuffer = file.getInboundBuffer();
when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
file.onReadReady(mockAsyncFile);
verify(mockAsyncFile).enableReadEvents(false);
verify(mockFileListener).onBufferedFileInboundData(eq(inboundBufferSize));
assertEquals(0, file.getOutboundBufferSize());
assertEquals(inboundBufferSize, inboundBuffer.size());
assertEquals((byte) 1, inboundBuffer.peek(0));
assertEquals((byte) 2, inboundBuffer.peek(data1.length));
assertEquals((byte) 3, inboundBuffer.peek(data1.length + data2.length));
checkAndResetMocks();
// Cannot enable read events since the buffer is full.
file.continueReading();
checkAndResetMocks();
final byte[] tmp = new byte[inboundBufferSize];
inboundBuffer.readBytes(tmp, 0, data1.length);
assertEquals(inboundBufferSize - data1.length, inboundBuffer.size());
file.continueReading();
inboundBuffer.readBytes(tmp, 0, data2.length);
assertEquals(inboundBufferSize - data1.length - data2.length, inboundBuffer.size());
when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
file.onReadReady(mockAsyncFile);
verify(mockAsyncFile, times(2)).enableReadEvents(true);
verify(mockFileListener).onBufferedFileInboundData(
eq(data1.length + data2.length + data3.length - inboundBufferSize));
assertEquals(data3.length, inboundBuffer.size());
assertEquals((byte) 3, inboundBuffer.peek(0));
}
@Test
public void shutdownReading() throws Exception {
final int inboundBufferSize = 250;
final int outboundBufferSize = 10;
final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
final byte[] data = new byte[100];
final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
file.shutdownReading();
file.onReadReady(mockAsyncFile);
verify(mockAsyncFile).enableReadEvents(false);
assertEquals(0, file.getInboundBuffer().size());
assertEquals(data.length, dataAnswer.getRemainingSize());
}
@Test
public void shutdownReading_inCallback() throws Exception {
final int inboundBufferSize = 250;
final int outboundBufferSize = 10;
final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
final byte[] data = new byte[100];
final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
doAnswer(new Answer() {
@Override public Object answer(InvocationOnMock invocation) {
file.shutdownReading();
return null;
}}).when(mockFileListener).onBufferedFileInboundData(anyInt());
file.onReadReady(mockAsyncFile);
verify(mockAsyncFile).enableReadEvents(false);
assertEquals(0, file.getInboundBuffer().size());
assertEquals(0, dataAnswer.getRemainingSize());
}
private void checkAndResetMocks() {
verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager,
mockParcelFileDescriptor));
reset(mockFileListener, mockAsyncFile, mockEventManager);
}
private BufferedFile createFile(
int inboundBufferSize, int outboundBufferSize) throws Exception {
when(mockEventManager.registerFile(any(), any())).thenReturn(mockAsyncFile);
return BufferedFile.create(
mockEventManager,
FileHandle.fromFileDescriptor(mockParcelFileDescriptor),
mockFileListener,
inboundBufferSize,
outboundBufferSize);
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 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.net.module.util.wear;
import static org.junit.Assert.assertEquals;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import com.android.net.module.util.async.CircularByteBuffer;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class NetPacketHelpersTest {
@Test
public void decodeNetworkUnsignedInt16() {
final byte[] data = new byte[4];
data[0] = (byte) 0xFF;
data[1] = (byte) 1;
data[2] = (byte) 2;
data[3] = (byte) 0xFF;
assertEquals(0x0102, NetPacketHelpers.decodeNetworkUnsignedInt16(data, 1));
CircularByteBuffer buffer = new CircularByteBuffer(100);
buffer.writeBytes(data, 0, data.length);
assertEquals(0x0102, NetPacketHelpers.decodeNetworkUnsignedInt16(buffer, 1));
}
@Test
public void encodeNetworkUnsignedInt16() {
final byte[] data = new byte[4];
data[0] = (byte) 0xFF;
data[3] = (byte) 0xFF;
NetPacketHelpers.encodeNetworkUnsignedInt16(0x0102, data, 1);
assertEquals((byte) 0xFF, data[0]);
assertEquals((byte) 1, data[1]);
assertEquals((byte) 2, data[2]);
assertEquals((byte) 0xFF, data[3]);
}
}

View file

@ -0,0 +1,291 @@
/*
* Copyright (C) 2023 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.net.module.util.wear;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.ignoreStubs;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.os.ParcelFileDescriptor;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.net.module.util.async.AsyncFile;
import com.android.net.module.util.async.BufferedFile;
import com.android.net.module.util.async.EventManager;
import com.android.net.module.util.async.FileHandle;
import com.android.net.module.util.async.ReadableByteBuffer;
import com.android.testutils.async.ReadableDataAnswer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class StreamingPacketFileTest {
private static final int MAX_PACKET_SIZE = 100;
@Mock EventManager mockEventManager;
@Mock PacketFile.Listener mockFileListener;
@Mock AsyncFile mockAsyncFile;
@Mock ParcelFileDescriptor mockParcelFileDescriptor;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
@After
public void tearDown() throws Exception {
verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager));
}
@Test
public void continueReadingAndClose() throws Exception {
final int maxBufferedInboundPackets = 3;
final int maxBufferedOutboundPackets = 5;
final StreamingPacketFile file =
createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
assertEquals(maxBufferedInboundPackets * (MAX_PACKET_SIZE + 2),
bufferedFile.getInboundBufferFreeSizeForTest());
assertEquals(maxBufferedOutboundPackets * (MAX_PACKET_SIZE + 2),
bufferedFile.getOutboundBufferFreeSize());
assertEquals(bufferedFile.getOutboundBufferFreeSize() - 2,
file.getOutboundFreeSize());
file.continueReading();
verify(mockAsyncFile).enableReadEvents(true);
file.close();
verify(mockAsyncFile).close();
}
@Test
public void enqueueOutboundPacket() throws Exception {
final int maxBufferedInboundPackets = 10;
final int maxBufferedOutboundPackets = 20;
final StreamingPacketFile file =
createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
final byte[] packet1 = new byte[11];
final byte[] packet2 = new byte[12];
packet1[0] = (byte) 1;
packet2[0] = (byte) 2;
assertEquals(0, bufferedFile.getOutboundBufferSize());
when(mockAsyncFile.write(any(), anyInt(), anyInt())).thenReturn(0);
assertTrue(file.enqueueOutboundPacket(packet1, 0, packet1.length));
verify(mockAsyncFile).enableWriteEvents(true);
assertEquals(packet1.length + 2, bufferedFile.getOutboundBufferSize());
checkAndResetMocks();
final int totalLen = packet1.length + packet2.length + 4;
final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
when(mockAsyncFile.write(
arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
assertTrue(file.enqueueOutboundPacket(packet2, 0, packet2.length));
assertEquals(0, bufferedFile.getInboundBuffer().size());
assertEquals(0, bufferedFile.getOutboundBufferSize());
assertEquals(0, posCaptor.getValue().intValue());
assertEquals(totalLen, lenCaptor.getValue().intValue());
final byte[] capturedData = arrayCaptor.getValue();
assertEquals(packet1.length, NetPacketHelpers.decodeNetworkUnsignedInt16(capturedData, 0));
assertEquals(packet2.length,
NetPacketHelpers.decodeNetworkUnsignedInt16(capturedData, packet1.length + 2));
assertEquals(packet1[0], capturedData[2]);
assertEquals(packet2[0], capturedData[packet1.length + 4]);
}
@Test
public void onInboundPacket() throws Exception {
final int maxBufferedInboundPackets = 10;
final int maxBufferedOutboundPackets = 20;
final StreamingPacketFile file =
createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
final ReadableByteBuffer inboundBuffer = bufferedFile.getInboundBuffer();
final int len1 = 11;
final int len2 = 12;
final byte[] data = new byte[len1 + len2 + 4];
NetPacketHelpers.encodeNetworkUnsignedInt16(len1, data, 0);
NetPacketHelpers.encodeNetworkUnsignedInt16(len2, data, 11 + 2);
data[2] = (byte) 1;
data[len1 + 4] = (byte) 2;
final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
when(mockFileListener.onPreambleData(any(), eq(0), eq(data.length))).thenReturn(0);
bufferedFile.onReadReady(mockAsyncFile);
verify(mockAsyncFile).enableReadEvents(true);
verify(mockFileListener).onInboundBuffered(data.length, data.length);
verify(mockFileListener).onInboundPacket(
arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
verify(mockEventManager).execute(any());
byte[] capturedData = arrayCaptor.getValue();
assertEquals(2, posCaptor.getValue().intValue());
assertEquals(len1, lenCaptor.getValue().intValue());
assertEquals((byte) 1, capturedData[2]);
checkAndResetMocks();
when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
file.onBufferedFileInboundData(0);
verify(mockFileListener).onInboundPacket(
arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
verify(mockEventManager).execute(any());
capturedData = arrayCaptor.getValue();
assertEquals(2, posCaptor.getValue().intValue());
assertEquals(len2, lenCaptor.getValue().intValue());
assertEquals((byte) 2, capturedData[2]);
assertEquals(0, bufferedFile.getOutboundBufferSize());
assertEquals(0, inboundBuffer.size());
}
@Test
public void onReadReady_preambleData() throws Exception {
final int maxBufferedInboundPackets = 10;
final int maxBufferedOutboundPackets = 20;
final StreamingPacketFile file =
createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
final ReadableByteBuffer inboundBuffer = bufferedFile.getInboundBuffer();
final int preambleLen = 23;
final int len1 = 11;
final byte[] data = new byte[preambleLen + 2 + len1];
NetPacketHelpers.encodeNetworkUnsignedInt16(len1, data, preambleLen);
data[preambleLen + 2] = (byte) 1;
final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
when(mockFileListener.onPreambleData(any(), eq(0), eq(data.length))).thenReturn(5);
when(mockFileListener.onPreambleData(
any(), eq(0), eq(data.length - 5))).thenReturn(preambleLen - 5);
when(mockFileListener.onPreambleData(
any(), eq(0), eq(data.length - preambleLen))).thenReturn(0);
bufferedFile.onReadReady(mockAsyncFile);
final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mockFileListener).onInboundBuffered(data.length, data.length);
verify(mockFileListener).onInboundPacket(
arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
verify(mockEventManager).execute(any());
verify(mockAsyncFile).enableReadEvents(true);
final byte[] capturedData = arrayCaptor.getValue();
assertEquals(2, posCaptor.getValue().intValue());
assertEquals(len1, lenCaptor.getValue().intValue());
assertEquals((byte) 1, capturedData[2]);
assertEquals(0, bufferedFile.getOutboundBufferSize());
assertEquals(0, inboundBuffer.size());
}
@Test
public void shutdownReading() throws Exception {
final int maxBufferedInboundPackets = 10;
final int maxBufferedOutboundPackets = 20;
final StreamingPacketFile file =
createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
final byte[] data = new byte[100];
final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
doAnswer(new Answer() {
@Override public Object answer(InvocationOnMock invocation) {
file.shutdownReading();
return Integer.valueOf(-1);
}}).when(mockFileListener).onPreambleData(any(), anyInt(), anyInt());
bufferedFile.onReadReady(mockAsyncFile);
verify(mockFileListener).onInboundBuffered(data.length, data.length);
verify(mockAsyncFile).enableReadEvents(false);
assertEquals(0, bufferedFile.getInboundBuffer().size());
}
private void checkAndResetMocks() {
verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager,
mockParcelFileDescriptor));
reset(mockFileListener, mockAsyncFile, mockEventManager);
}
private StreamingPacketFile createFile(
int maxBufferedInboundPackets, int maxBufferedOutboundPackets) throws Exception {
when(mockEventManager.registerFile(any(), any())).thenReturn(mockAsyncFile);
return new StreamingPacketFile(
mockEventManager,
FileHandle.fromFileDescriptor(mockParcelFileDescriptor),
mockFileListener,
MAX_PACKET_SIZE,
maxBufferedInboundPackets,
maxBufferedOutboundPackets);
}
}

View file

@ -38,6 +38,7 @@ java_library {
"net-utils-device-common",
"net-utils-device-common-async",
"net-utils-device-common-netlink",
"net-utils-device-common-wear",
"modules-utils-build_system",
],
lint: { strict_updatability_linting: true },

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.testutils;
package com.android.testutils.async;
import android.os.ParcelFileDescriptor;
import android.system.StructPollfd;

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.testutils;
package com.android.testutils.async;
import com.android.net.module.util.async.OsAccess;

View file

@ -0,0 +1,76 @@
/*
* Copyright (C) 2023 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.testutils.async;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
public class ReadableDataAnswer implements Answer {
private final ArrayList<byte[]> mBuffers = new ArrayList<>();
private int mBufferPos;
public ReadableDataAnswer(byte[] ... buffers) {
for (byte[] buffer : buffers) {
addBuffer(buffer);
}
}
public void addBuffer(byte[] buffer) {
if (buffer.length != 0) {
mBuffers.add(buffer);
}
}
public int getRemainingSize() {
int totalSize = 0;
for (byte[] buffer : mBuffers) {
totalSize += buffer.length;
}
return totalSize - mBufferPos;
}
private void cleanupBuffers() {
if (!mBuffers.isEmpty() && mBufferPos == mBuffers.get(0).length) {
mBuffers.remove(0);
mBufferPos = 0;
}
}
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
cleanupBuffers();
if (mBuffers.isEmpty()) {
return Integer.valueOf(0);
}
byte[] src = mBuffers.get(0);
byte[] dst = invocation.<byte[]>getArgument(0);
int dstPos = invocation.<Integer>getArgument(1);
int dstLen = invocation.<Integer>getArgument(2);
int copyLen = Math.min(dstLen, src.length - mBufferPos);
System.arraycopy(src, mBufferPos, dst, dstPos, copyLen);
mBufferPos += copyLen;
cleanupBuffers();
return Integer.valueOf(copyLen);
}
}