/* * Copyright (C) 2019 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 android.util; import static java.nio.charset.StandardCharsets.UTF_8; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.os.Build; import android.os.SystemClock; import androidx.annotation.RequiresApi; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.nio.ByteBuffer; import java.util.Arrays; /** * StatsEvent builds and stores the buffer sent over the statsd socket. * This class defines and encapsulates the socket protocol. * *

Usage:

*
 *      // Pushed event
 *      StatsEvent statsEvent = StatsEvent.newBuilder()
 *          .setAtomId(atomId)
 *          .writeBoolean(false)
 *          .writeString("annotated String field")
 *          .addBooleanAnnotation(annotationId, true)
 *          .usePooledBuffer()
 *          .build();
 *      StatsLog.write(statsEvent);
 *
 *      // Pulled event
 *      StatsEvent statsEvent = StatsEvent.newBuilder()
 *          .setAtomId(atomId)
 *          .writeBoolean(false)
 *          .writeString("annotated String field")
 *          .addBooleanAnnotation(annotationId, true)
 *          .build();
 * 
* @hide **/ @SystemApi public final class StatsEvent { // Type Ids. /** * @hide **/ @VisibleForTesting public static final byte TYPE_INT = 0x00; /** * @hide **/ @VisibleForTesting public static final byte TYPE_LONG = 0x01; /** * @hide **/ @VisibleForTesting public static final byte TYPE_STRING = 0x02; /** * @hide **/ @VisibleForTesting public static final byte TYPE_LIST = 0x03; /** * @hide **/ @VisibleForTesting public static final byte TYPE_FLOAT = 0x04; /** * @hide **/ @VisibleForTesting public static final byte TYPE_BOOLEAN = 0x05; /** * @hide **/ @VisibleForTesting public static final byte TYPE_BYTE_ARRAY = 0x06; /** * @hide **/ @VisibleForTesting public static final byte TYPE_OBJECT = 0x07; /** * @hide **/ @VisibleForTesting public static final byte TYPE_KEY_VALUE_PAIRS = 0x08; /** * @hide **/ @VisibleForTesting public static final byte TYPE_ATTRIBUTION_CHAIN = 0x09; /** * @hide **/ @VisibleForTesting public static final byte TYPE_ERRORS = 0x0F; // Error flags. /** * @hide **/ @VisibleForTesting public static final int ERROR_NO_TIMESTAMP = 0x1; /** * @hide **/ @VisibleForTesting public static final int ERROR_NO_ATOM_ID = 0x2; /** * @hide **/ @VisibleForTesting public static final int ERROR_OVERFLOW = 0x4; /** * @hide **/ @VisibleForTesting public static final int ERROR_ATTRIBUTION_CHAIN_TOO_LONG = 0x8; /** * @hide **/ @VisibleForTesting public static final int ERROR_TOO_MANY_KEY_VALUE_PAIRS = 0x10; /** * @hide **/ @VisibleForTesting public static final int ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD = 0x20; /** * @hide **/ @VisibleForTesting public static final int ERROR_INVALID_ANNOTATION_ID = 0x40; /** * @hide **/ @VisibleForTesting public static final int ERROR_ANNOTATION_ID_TOO_LARGE = 0x80; /** * @hide **/ @VisibleForTesting public static final int ERROR_TOO_MANY_ANNOTATIONS = 0x100; /** * @hide **/ @VisibleForTesting public static final int ERROR_TOO_MANY_FIELDS = 0x200; /** * @hide **/ @VisibleForTesting public static final int ERROR_ATTRIBUTION_UIDS_TAGS_SIZES_NOT_EQUAL = 0x1000; /** * @hide **/ @VisibleForTesting public static final int ERROR_ATOM_ID_INVALID_POSITION = 0x2000; /** * @hide **/ @VisibleForTesting public static final int ERROR_LIST_TOO_LONG = 0x4000; // Size limits. /** * @hide **/ @VisibleForTesting public static final int MAX_ANNOTATION_COUNT = 15; /** * @hide **/ @VisibleForTesting public static final int MAX_ATTRIBUTION_NODES = 127; /** * @hide **/ @VisibleForTesting public static final int MAX_NUM_ELEMENTS = 127; /** * @hide **/ @VisibleForTesting public static final int MAX_KEY_VALUE_PAIRS = 127; private static final int LOGGER_ENTRY_MAX_PAYLOAD = 4068; // Max payload size is 4 bytes less as 4 bytes are reserved for statsEventTag. // See android_util_StatsLog.cpp. private static final int MAX_PUSH_PAYLOAD_SIZE = LOGGER_ENTRY_MAX_PAYLOAD - 4; private static final int MAX_PULL_PAYLOAD_SIZE = 50 * 1024; // 50 KB private final int mAtomId; private final byte[] mPayload; private Buffer mBuffer; private final int mNumBytes; private StatsEvent(final int atomId, @Nullable final Buffer buffer, @NonNull final byte[] payload, final int numBytes) { mAtomId = atomId; mBuffer = buffer; mPayload = payload; mNumBytes = numBytes; } /** * Returns a new StatsEvent.Builder for building StatsEvent object. **/ @NonNull public static StatsEvent.Builder newBuilder() { return new StatsEvent.Builder(Buffer.obtain()); } /** * Get the atom Id of the atom encoded in this StatsEvent object. * * @hide **/ public int getAtomId() { return mAtomId; } /** * Get the byte array that contains the encoded payload that can be sent to statsd. * * @hide **/ @NonNull public byte[] getBytes() { return mPayload; } /** * Get the number of bytes used to encode the StatsEvent payload. * * @hide **/ public int getNumBytes() { return mNumBytes; } /** * Recycle resources used by this StatsEvent object. * No actions should be taken on this StatsEvent after release() is called. * * @hide **/ public void release() { if (mBuffer != null) { mBuffer.release(); mBuffer = null; } } /** * Builder for constructing a StatsEvent object. * *

This class defines and encapsulates the socket encoding for the *buffer. The write methods must be called in the same order as the order of *fields in the atom definition.

* *

setAtomId() must be called immediately after *StatsEvent.newBuilder().

* *

Example:

*
     *     // Atom definition.
     *     message MyAtom {
     *         optional int32 field1 = 1;
     *         optional int64 field2 = 2;
     *         optional string field3 = 3 [(annotation1) = true];
     *         optional repeated int32 field4 = 4;
     *     }
     *
     *     // StatsEvent construction for pushed event.
     *     StatsEvent.newBuilder()
     *     StatsEvent statsEvent = StatsEvent.newBuilder()
     *         .setAtomId(atomId)
     *         .writeInt(3) // field1
     *         .writeLong(8L) // field2
     *         .writeString("foo") // field 3
     *         .addBooleanAnnotation(annotation1Id, true)
     *         .writeIntArray({ 1, 2, 3 });
     *         .usePooledBuffer()
     *         .build();
     *
     *     // StatsEvent construction for pulled event.
     *     StatsEvent.newBuilder()
     *     StatsEvent statsEvent = StatsEvent.newBuilder()
     *         .setAtomId(atomId)
     *         .writeInt(3) // field1
     *         .writeLong(8L) // field2
     *         .writeString("foo") // field 3
     *         .addBooleanAnnotation(annotation1Id, true)
     *         .writeIntArray({ 1, 2, 3 });
     *         .build();
     * 
**/ public static final class Builder { // Fixed positions. private static final int POS_NUM_ELEMENTS = 1; private static final int POS_TIMESTAMP_NS = POS_NUM_ELEMENTS + Byte.BYTES; private static final int POS_ATOM_ID = POS_TIMESTAMP_NS + Byte.BYTES + Long.BYTES; private final Buffer mBuffer; private long mTimestampNs; private int mAtomId; private byte mCurrentAnnotationCount; private int mPos; private int mPosLastField; private byte mLastType; private int mNumElements; private int mErrorMask; private boolean mUsePooledBuffer = false; private Builder(final Buffer buffer) { mBuffer = buffer; mCurrentAnnotationCount = 0; mAtomId = 0; mTimestampNs = SystemClock.elapsedRealtimeNanos(); mNumElements = 0; // Set mPos to 0 for writing TYPE_OBJECT at 0th position. mPos = 0; writeTypeId(TYPE_OBJECT); // Write timestamp. mPos = POS_TIMESTAMP_NS; writeLong(mTimestampNs); } /** * Sets the atom id for this StatsEvent. * * This should be called immediately after StatsEvent.newBuilder() * and should only be called once. * Not calling setAtomId will result in ERROR_NO_ATOM_ID. * Calling setAtomId out of order will result in ERROR_ATOM_ID_INVALID_POSITION. **/ @NonNull public Builder setAtomId(final int atomId) { if (0 == mAtomId) { mAtomId = atomId; if (1 == mNumElements) { // Only timestamp is written so far. writeInt(atomId); } else { // setAtomId called out of order. mErrorMask |= ERROR_ATOM_ID_INVALID_POSITION; } } return this; } /** * Write a boolean field to this StatsEvent. **/ @NonNull public Builder writeBoolean(final boolean value) { // Write boolean typeId byte followed by boolean byte representation. writeTypeId(TYPE_BOOLEAN); mPos += mBuffer.putBoolean(mPos, value); mNumElements++; return this; } /** * Write an integer field to this StatsEvent. **/ @NonNull public Builder writeInt(final int value) { // Write integer typeId byte followed by 4-byte representation of value. writeTypeId(TYPE_INT); mPos += mBuffer.putInt(mPos, value); mNumElements++; return this; } /** * Write a long field to this StatsEvent. **/ @NonNull public Builder writeLong(final long value) { // Write long typeId byte followed by 8-byte representation of value. writeTypeId(TYPE_LONG); mPos += mBuffer.putLong(mPos, value); mNumElements++; return this; } /** * Write a float field to this StatsEvent. **/ @NonNull public Builder writeFloat(final float value) { // Write float typeId byte followed by 4-byte representation of value. writeTypeId(TYPE_FLOAT); mPos += mBuffer.putFloat(mPos, value); mNumElements++; return this; } /** * Write a String field to this StatsEvent. **/ @NonNull public Builder writeString(@NonNull final String value) { // Write String typeId byte, followed by 4-byte representation of number of bytes // in the UTF-8 encoding, followed by the actual UTF-8 byte encoding of value. final byte[] valueBytes = stringToBytes(value); writeByteArray(valueBytes, TYPE_STRING); return this; } /** * Write a byte array field to this StatsEvent. **/ @NonNull public Builder writeByteArray(@NonNull final byte[] value) { // Write byte array typeId byte, followed by 4-byte representation of number of bytes // in value, followed by the actual byte array. writeByteArray(value, TYPE_BYTE_ARRAY); return this; } private void writeByteArray(@NonNull final byte[] value, final byte typeId) { writeTypeId(typeId); final int numBytes = value.length; mPos += mBuffer.putInt(mPos, numBytes); mPos += mBuffer.putByteArray(mPos, value); mNumElements++; } /** * Write an attribution chain field to this StatsEvent. * * The sizes of uids and tags must be equal. The AttributionNode at position i is * made up of uids[i] and tags[i]. * * @param uids array of uids in the attribution nodes. * @param tags array of tags in the attribution nodes. **/ @NonNull public Builder writeAttributionChain( @NonNull final int[] uids, @NonNull final String[] tags) { final byte numUids = (byte) uids.length; final byte numTags = (byte) tags.length; if (numUids != numTags) { mErrorMask |= ERROR_ATTRIBUTION_UIDS_TAGS_SIZES_NOT_EQUAL; } else if (numUids > MAX_ATTRIBUTION_NODES) { mErrorMask |= ERROR_ATTRIBUTION_CHAIN_TOO_LONG; } else { // Write attribution chain typeId byte, followed by 1-byte representation of // number of attribution nodes, followed by encoding of each attribution node. writeTypeId(TYPE_ATTRIBUTION_CHAIN); mPos += mBuffer.putByte(mPos, numUids); for (int i = 0; i < numUids; i++) { // Each uid is encoded as 4-byte representation of its int value. mPos += mBuffer.putInt(mPos, uids[i]); // Each tag is encoded as 4-byte representation of number of bytes in its // UTF-8 encoding, followed by the actual UTF-8 bytes. final byte[] tagBytes = stringToBytes(tags[i]); mPos += mBuffer.putInt(mPos, tagBytes.length); mPos += mBuffer.putByteArray(mPos, tagBytes); } mNumElements++; } return this; } /** * Write KeyValuePairsAtom entries to this StatsEvent. * * @param intMap Integer key-value pairs. * @param longMap Long key-value pairs. * @param stringMap String key-value pairs. * @param floatMap Float key-value pairs. **/ @NonNull public Builder writeKeyValuePairs( @Nullable final SparseIntArray intMap, @Nullable final SparseLongArray longMap, @Nullable final SparseArray stringMap, @Nullable final SparseArray floatMap) { final int intMapSize = null == intMap ? 0 : intMap.size(); final int longMapSize = null == longMap ? 0 : longMap.size(); final int stringMapSize = null == stringMap ? 0 : stringMap.size(); final int floatMapSize = null == floatMap ? 0 : floatMap.size(); final int totalCount = intMapSize + longMapSize + stringMapSize + floatMapSize; if (totalCount > MAX_KEY_VALUE_PAIRS) { mErrorMask |= ERROR_TOO_MANY_KEY_VALUE_PAIRS; } else { writeTypeId(TYPE_KEY_VALUE_PAIRS); mPos += mBuffer.putByte(mPos, (byte) totalCount); for (int i = 0; i < intMapSize; i++) { final int key = intMap.keyAt(i); final int value = intMap.valueAt(i); mPos += mBuffer.putInt(mPos, key); writeTypeId(TYPE_INT); mPos += mBuffer.putInt(mPos, value); } for (int i = 0; i < longMapSize; i++) { final int key = longMap.keyAt(i); final long value = longMap.valueAt(i); mPos += mBuffer.putInt(mPos, key); writeTypeId(TYPE_LONG); mPos += mBuffer.putLong(mPos, value); } for (int i = 0; i < stringMapSize; i++) { final int key = stringMap.keyAt(i); final String value = stringMap.valueAt(i); mPos += mBuffer.putInt(mPos, key); writeTypeId(TYPE_STRING); final byte[] valueBytes = stringToBytes(value); mPos += mBuffer.putInt(mPos, valueBytes.length); mPos += mBuffer.putByteArray(mPos, valueBytes); } for (int i = 0; i < floatMapSize; i++) { final int key = floatMap.keyAt(i); final float value = floatMap.valueAt(i); mPos += mBuffer.putInt(mPos, key); writeTypeId(TYPE_FLOAT); mPos += mBuffer.putFloat(mPos, value); } mNumElements++; } return this; } /** * Write a repeated boolean field to this StatsEvent. * * The list size must not exceed 127. Otherwise, the array isn't written * to the StatsEvent and ERROR_LIST_TOO_LONG is appended to the * StatsEvent errors field. * * @param elements array of booleans. **/ @RequiresApi(Build.VERSION_CODES.TIRAMISU) @NonNull public Builder writeBooleanArray(@NonNull final boolean[] elements) { final byte numElements = (byte)elements.length; if (writeArrayInfo(numElements, TYPE_BOOLEAN)) { // Write encoding of each element. for (int i = 0; i < numElements; i++) { mPos += mBuffer.putBoolean(mPos, elements[i]); } mNumElements++; } return this; } /** * Write a repeated int field to this StatsEvent. * * The list size must not exceed 127. Otherwise, the array isn't written * to the StatsEvent and ERROR_LIST_TOO_LONG is appended to the * StatsEvent errors field. * * @param elements array of ints. **/ @RequiresApi(Build.VERSION_CODES.TIRAMISU) @NonNull public Builder writeIntArray(@NonNull final int[] elements) { final byte numElements = (byte)elements.length; if (writeArrayInfo(numElements, TYPE_INT)) { // Write encoding of each element. for (int i = 0; i < numElements; i++) { mPos += mBuffer.putInt(mPos, elements[i]); } mNumElements++; } return this; } /** * Write a repeated long field to this StatsEvent. * * The list size must not exceed 127. Otherwise, the array isn't written * to the StatsEvent and ERROR_LIST_TOO_LONG is appended to the * StatsEvent errors field. * * @param elements array of longs. **/ @RequiresApi(Build.VERSION_CODES.TIRAMISU) @NonNull public Builder writeLongArray(@NonNull final long[] elements) { final byte numElements = (byte)elements.length; if (writeArrayInfo(numElements, TYPE_LONG)) { // Write encoding of each element. for (int i = 0; i < numElements; i++) { mPos += mBuffer.putLong(mPos, elements[i]); } mNumElements++; } return this; } /** * Write a repeated float field to this StatsEvent. * * The list size must not exceed 127. Otherwise, the array isn't written * to the StatsEvent and ERROR_LIST_TOO_LONG is appended to the * StatsEvent errors field. * * @param elements array of floats. **/ @RequiresApi(Build.VERSION_CODES.TIRAMISU) @NonNull public Builder writeFloatArray(@NonNull final float[] elements) { final byte numElements = (byte)elements.length; if (writeArrayInfo(numElements, TYPE_FLOAT)) { // Write encoding of each element. for (int i = 0; i < numElements; i++) { mPos += mBuffer.putFloat(mPos, elements[i]); } mNumElements++; } return this; } /** * Write a repeated string field to this StatsEvent. * * The list size must not exceed 127. Otherwise, the array isn't written * to the StatsEvent and ERROR_LIST_TOO_LONG is appended to the * StatsEvent errors field. * * @param elements array of strings. **/ @RequiresApi(Build.VERSION_CODES.TIRAMISU) @NonNull public Builder writeStringArray(@NonNull final String[] elements) { final byte numElements = (byte)elements.length; if (writeArrayInfo(numElements, TYPE_STRING)) { // Write encoding of each element. for (int i = 0; i < numElements; i++) { final byte[] elementBytes = stringToBytes(elements[i]); mPos += mBuffer.putInt(mPos, elementBytes.length); mPos += mBuffer.putByteArray(mPos, elementBytes); } mNumElements++; } return this; } /** * Write a boolean annotation for the last field written. **/ @NonNull public Builder addBooleanAnnotation( final byte annotationId, final boolean value) { // Ensure there's a field written to annotate. if (mNumElements < 2) { mErrorMask |= ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD; } else if (mCurrentAnnotationCount >= MAX_ANNOTATION_COUNT) { mErrorMask |= ERROR_TOO_MANY_ANNOTATIONS; } else { mPos += mBuffer.putByte(mPos, annotationId); mPos += mBuffer.putByte(mPos, TYPE_BOOLEAN); mPos += mBuffer.putBoolean(mPos, value); mCurrentAnnotationCount++; writeAnnotationCount(); } return this; } /** * Write an integer annotation for the last field written. **/ @NonNull public Builder addIntAnnotation(final byte annotationId, final int value) { if (mNumElements < 2) { mErrorMask |= ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD; } else if (mCurrentAnnotationCount >= MAX_ANNOTATION_COUNT) { mErrorMask |= ERROR_TOO_MANY_ANNOTATIONS; } else { mPos += mBuffer.putByte(mPos, annotationId); mPos += mBuffer.putByte(mPos, TYPE_INT); mPos += mBuffer.putInt(mPos, value); mCurrentAnnotationCount++; writeAnnotationCount(); } return this; } /** * Indicates to reuse Buffer's byte array as the underlying payload in StatsEvent. * This should be called for pushed events to reduce memory allocations and garbage * collections. **/ @NonNull public Builder usePooledBuffer() { mUsePooledBuffer = true; mBuffer.setMaxSize(MAX_PUSH_PAYLOAD_SIZE, mPos); return this; } /** * Builds a StatsEvent object with values entered in this Builder. **/ @NonNull public StatsEvent build() { if (0L == mTimestampNs) { mErrorMask |= ERROR_NO_TIMESTAMP; } if (0 == mAtomId) { mErrorMask |= ERROR_NO_ATOM_ID; } if (mBuffer.hasOverflowed()) { mErrorMask |= ERROR_OVERFLOW; } if (mNumElements > MAX_NUM_ELEMENTS) { mErrorMask |= ERROR_TOO_MANY_FIELDS; } if (0 == mErrorMask) { mBuffer.putByte(POS_NUM_ELEMENTS, (byte) mNumElements); } else { // Write atom id and error mask. Overwrite any annotations for atom Id. mPos = POS_ATOM_ID; mPos += mBuffer.putByte(mPos, TYPE_INT); mPos += mBuffer.putInt(mPos, mAtomId); mPos += mBuffer.putByte(mPos, TYPE_ERRORS); mPos += mBuffer.putInt(mPos, mErrorMask); mBuffer.putByte(POS_NUM_ELEMENTS, (byte) 3); } final int size = mPos; if (mUsePooledBuffer) { return new StatsEvent(mAtomId, mBuffer, mBuffer.getBytes(), size); } else { // Create a copy of the buffer with the required number of bytes. final byte[] payload = new byte[size]; System.arraycopy(mBuffer.getBytes(), 0, payload, 0, size); // Return Buffer instance to the pool. mBuffer.release(); return new StatsEvent(mAtomId, null, payload, size); } } private void writeTypeId(final byte typeId) { mPosLastField = mPos; mLastType = typeId; mCurrentAnnotationCount = 0; final byte encodedId = (byte) (typeId & 0x0F); mPos += mBuffer.putByte(mPos, encodedId); } private void writeAnnotationCount() { // Use first 4 bits for annotation count and last 4 bits for typeId. final byte encodedId = (byte) ((mCurrentAnnotationCount << 4) | (mLastType & 0x0F)); mBuffer.putByte(mPosLastField, encodedId); } @NonNull private static byte[] stringToBytes(@Nullable final String value) { return (null == value ? "" : value).getBytes(UTF_8); } private boolean writeArrayInfo(final byte numElements, final byte elementTypeId) { if (numElements > MAX_NUM_ELEMENTS) { mErrorMask |= ERROR_LIST_TOO_LONG; return false; } // Write list typeId byte, 1-byte representation of number of // elements, and element typeId byte. writeTypeId(TYPE_LIST); mPos += mBuffer.putByte(mPos, numElements); // Write element typeId byte without setting mPosLastField and mLastType (i.e. don't use // #writeTypeId) final byte encodedId = (byte) (elementTypeId & 0x0F); mPos += mBuffer.putByte(mPos, encodedId); return true; } } private static final class Buffer { private static Object sLock = new Object(); @GuardedBy("sLock") private static Buffer sPool; private byte[] mBytes; private boolean mOverflow = false; private int mMaxSize = MAX_PULL_PAYLOAD_SIZE; @NonNull private static Buffer obtain() { final Buffer buffer; synchronized (sLock) { buffer = null == sPool ? new Buffer() : sPool; sPool = null; } buffer.reset(); return buffer; } private Buffer() { final ByteBuffer tempBuffer = ByteBuffer.allocateDirect(MAX_PUSH_PAYLOAD_SIZE); mBytes = tempBuffer.hasArray() ? tempBuffer.array() : new byte [MAX_PUSH_PAYLOAD_SIZE]; } @NonNull private byte[] getBytes() { return mBytes; } private void release() { // Recycle this Buffer if its size is MAX_PUSH_PAYLOAD_SIZE or under. if (mMaxSize <= MAX_PUSH_PAYLOAD_SIZE) { synchronized (sLock) { if (null == sPool) { sPool = this; } } } } private void reset() { mOverflow = false; mMaxSize = MAX_PULL_PAYLOAD_SIZE; } private void setMaxSize(final int maxSize, final int numBytesWritten) { mMaxSize = maxSize; if (numBytesWritten > maxSize) { mOverflow = true; } } private boolean hasOverflowed() { return mOverflow; } /** * Checks for available space in the byte array. * * @param index starting position in the buffer to start the check. * @param numBytes number of bytes to check from index. * @return true if space is available, false otherwise. **/ private boolean hasEnoughSpace(final int index, final int numBytes) { final int totalBytesNeeded = index + numBytes; if (totalBytesNeeded > mMaxSize) { mOverflow = true; return false; } // Expand buffer if needed. if (mBytes.length < mMaxSize && totalBytesNeeded > mBytes.length) { int newSize = mBytes.length; do { newSize *= 2; } while (newSize <= totalBytesNeeded); if (newSize > mMaxSize) { newSize = mMaxSize; } mBytes = Arrays.copyOf(mBytes, newSize); } return true; } /** * Writes a byte into the buffer. * * @param index position in the buffer where the byte is written. * @param value the byte to write. * @return number of bytes written to buffer from this write operation. **/ private int putByte(final int index, final byte value) { if (hasEnoughSpace(index, Byte.BYTES)) { mBytes[index] = (byte) (value); return Byte.BYTES; } return 0; } /** * Writes a boolean into the buffer. * * @param index position in the buffer where the boolean is written. * @param value the boolean to write. * @return number of bytes written to buffer from this write operation. **/ private int putBoolean(final int index, final boolean value) { return putByte(index, (byte) (value ? 1 : 0)); } /** * Writes an integer into the buffer. * * @param index position in the buffer where the integer is written. * @param value the integer to write. * @return number of bytes written to buffer from this write operation. **/ private int putInt(final int index, final int value) { if (hasEnoughSpace(index, Integer.BYTES)) { // Use little endian byte order. mBytes[index] = (byte) (value); mBytes[index + 1] = (byte) (value >> 8); mBytes[index + 2] = (byte) (value >> 16); mBytes[index + 3] = (byte) (value >> 24); return Integer.BYTES; } return 0; } /** * Writes a long into the buffer. * * @param index position in the buffer where the long is written. * @param value the long to write. * @return number of bytes written to buffer from this write operation. **/ private int putLong(final int index, final long value) { if (hasEnoughSpace(index, Long.BYTES)) { // Use little endian byte order. mBytes[index] = (byte) (value); mBytes[index + 1] = (byte) (value >> 8); mBytes[index + 2] = (byte) (value >> 16); mBytes[index + 3] = (byte) (value >> 24); mBytes[index + 4] = (byte) (value >> 32); mBytes[index + 5] = (byte) (value >> 40); mBytes[index + 6] = (byte) (value >> 48); mBytes[index + 7] = (byte) (value >> 56); return Long.BYTES; } return 0; } /** * Writes a float into the buffer. * * @param index position in the buffer where the float is written. * @param value the float to write. * @return number of bytes written to buffer from this write operation. **/ private int putFloat(final int index, final float value) { return putInt(index, Float.floatToIntBits(value)); } /** * Copies a byte array into the buffer. * * @param index position in the buffer where the byte array is copied. * @param value the byte array to copy. * @return number of bytes written to buffer from this write operation. **/ private int putByteArray(final int index, @NonNull final byte[] value) { final int numBytes = value.length; if (hasEnoughSpace(index, numBytes)) { System.arraycopy(value, 0, mBytes, index, numBytes); return numBytes; } return 0; } } }