| /* |
| * Copyright 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.media; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.os.Bundle; |
| |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Objects; |
| |
| /** |
| * MediaMetrics is the Java interface to the MediaMetrics service. |
| * |
| * This is used to collect media statistics by the framework. |
| * It is not intended for direct application use. |
| * |
| * @hide |
| */ |
| public class MediaMetrics { |
| public static final String TAG = "MediaMetrics"; |
| |
| public static final String SEPARATOR = "."; |
| |
| /** |
| * A list of established MediaMetrics names that can be used for Items. |
| */ |
| public static class Name { |
| public static final String AUDIO = "audio"; |
| public static final String AUDIO_BLUETOOTH = AUDIO + SEPARATOR + "bluetooth"; |
| public static final String AUDIO_DEVICE = AUDIO + SEPARATOR + "device"; |
| public static final String AUDIO_FOCUS = AUDIO + SEPARATOR + "focus"; |
| public static final String AUDIO_FORCE_USE = AUDIO + SEPARATOR + "forceUse"; |
| public static final String AUDIO_MIC = AUDIO + SEPARATOR + "mic"; |
| public static final String AUDIO_SERVICE = AUDIO + SEPARATOR + "service"; |
| public static final String AUDIO_VOLUME = AUDIO + SEPARATOR + "volume"; |
| public static final String AUDIO_VOLUME_EVENT = AUDIO_VOLUME + SEPARATOR + "event"; |
| public static final String AUDIO_MODE = AUDIO + SEPARATOR + "mode"; |
| public static final String METRICS_MANAGER = "metrics" + SEPARATOR + "manager"; |
| } |
| |
| /** |
| * A list of established string values. |
| */ |
| public static class Value { |
| public static final String CONNECT = "connect"; |
| public static final String CONNECTED = "connected"; |
| public static final String DISCONNECT = "disconnect"; |
| public static final String DISCONNECTED = "disconnected"; |
| public static final String DOWN = "down"; |
| public static final String MUTE = "mute"; |
| public static final String NO = "no"; |
| public static final String OFF = "off"; |
| public static final String ON = "on"; |
| public static final String UNMUTE = "unmute"; |
| public static final String UP = "up"; |
| public static final String YES = "yes"; |
| } |
| |
| /** |
| * A list of standard property keys for consistent use and type. |
| */ |
| public static class Property { |
| // A use for Bluetooth or USB device addresses |
| public static final Key<String> ADDRESS = createKey("address", String.class); |
| // A string representing the Audio Attributes |
| public static final Key<String> ATTRIBUTES = createKey("attributes", String.class); |
| |
| // The calling package responsible for the state change |
| public static final Key<String> CALLING_PACKAGE = |
| createKey("callingPackage", String.class); |
| |
| // The client name |
| public static final Key<String> CLIENT_NAME = createKey("clientName", String.class); |
| |
| // The device type |
| public static final Key<Integer> DELAY_MS = createKey("delayMs", Integer.class); |
| |
| // The device type |
| public static final Key<String> DEVICE = createKey("device", String.class); |
| |
| // For volume changes, up or down |
| public static final Key<String> DIRECTION = createKey("direction", String.class); |
| |
| // A reason for early return or error |
| public static final Key<String> EARLY_RETURN = |
| createKey("earlyReturn", String.class); |
| // ENCODING_ ... string to match AudioFormat encoding |
| public static final Key<String> ENCODING = createKey("encoding", String.class); |
| |
| public static final Key<String> EVENT = createKey("event#", String.class); |
| |
| // event generated is external (yes, no) |
| public static final Key<String> EXTERNAL = createKey("external", String.class); |
| |
| public static final Key<Integer> FLAGS = createKey("flags", Integer.class); |
| public static final Key<String> FOCUS_CHANGE_HINT = |
| createKey("focusChangeHint", String.class); |
| public static final Key<String> FORCE_USE_DUE_TO = |
| createKey("forceUseDueTo", String.class); |
| public static final Key<String> FORCE_USE_MODE = |
| createKey("forceUseMode", String.class); |
| public static final Key<Double> GAIN_DB = |
| createKey("gainDb", Double.class); |
| public static final Key<String> GROUP = |
| createKey("group", String.class); |
| |
| public static final Key<Integer> INDEX = createKey("index", Integer.class); // volume |
| public static final Key<String> LOG_SESSION_ID = createKey("logSessionId", String.class); |
| public static final Key<Integer> MAX_INDEX = createKey("maxIndex", Integer.class); // vol |
| public static final Key<Integer> MIN_INDEX = createKey("minIndex", Integer.class); // vol |
| public static final Key<String> MODE = |
| createKey("mode", String.class); // audio_mode |
| public static final Key<String> MUTE = |
| createKey("mute", String.class); // microphone, on or off. |
| |
| // Bluetooth or Usb device name |
| public static final Key<String> NAME = |
| createKey("name", String.class); |
| |
| // Number of observers |
| public static final Key<Integer> OBSERVERS = |
| createKey("observers", Integer.class); |
| |
| public static final Key<String> REQUEST = |
| createKey("request", String.class); |
| |
| // For audio mode |
| public static final Key<String> REQUESTED_MODE = |
| createKey("requestedMode", String.class); // audio_mode |
| |
| // For Bluetooth |
| public static final Key<String> SCO_AUDIO_MODE = |
| createKey("scoAudioMode", String.class); |
| public static final Key<Integer> SDK = createKey("sdk", Integer.class); |
| public static final Key<String> STATE = createKey("state", String.class); |
| public static final Key<Integer> STATUS = createKey("status", Integer.class); |
| public static final Key<String> STREAM_TYPE = createKey("streamType", String.class); |
| } |
| |
| /** |
| * The TYPE constants below should match those in native MediaMetricsItem.h |
| */ |
| private static final int TYPE_NONE = 0; |
| private static final int TYPE_INT32 = 1; // Java integer |
| private static final int TYPE_INT64 = 2; // Java long |
| private static final int TYPE_DOUBLE = 3; // Java double |
| private static final int TYPE_CSTRING = 4; // Java string |
| private static final int TYPE_RATE = 5; // Two longs, ignored in Java |
| |
| // The charset used for encoding Strings to bytes. |
| private static final Charset MEDIAMETRICS_CHARSET = StandardCharsets.UTF_8; |
| |
| /** |
| * Key interface. |
| * |
| * The presence of this {@code Key} interface on an object allows |
| * it to be used to set metrics. |
| * |
| * @param <T> type of value associated with {@code Key}. |
| */ |
| public interface Key<T> { |
| /** |
| * Returns the internal name of the key. |
| */ |
| @NonNull |
| String getName(); |
| |
| /** |
| * Returns the class type of the associated value. |
| */ |
| @NonNull |
| Class<T> getValueClass(); |
| } |
| |
| /** |
| * Returns a Key object with the correct interface for MediaMetrics. |
| * |
| * @param name The name of the key. |
| * @param type The class type of the value represented by the key. |
| * @param <T> The type of value. |
| * @return a new key interface. |
| */ |
| @NonNull |
| public static <T> Key<T> createKey(@NonNull String name, @NonNull Class<T> type) { |
| // Implementation specific. |
| return new Key<T>() { |
| private final String mName = name; |
| private final Class<T> mType = type; |
| |
| @Override |
| @NonNull |
| public String getName() { |
| return mName; |
| } |
| |
| @Override |
| @NonNull |
| public Class<T> getValueClass() { |
| return mType; |
| } |
| |
| /** |
| * Return true if the name and the type of two objects are the same. |
| */ |
| @Override |
| public boolean equals(Object obj) { |
| if (obj == this) { |
| return true; |
| } |
| if (!(obj instanceof Key)) { |
| return false; |
| } |
| Key<?> other = (Key<?>) obj; |
| return mName.equals(other.getName()) && mType.equals(other.getValueClass()); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mName, mType); |
| } |
| }; |
| } |
| |
| /** |
| * Item records properties and delivers to the MediaMetrics service |
| * |
| */ |
| public static class Item { |
| |
| /* |
| * MediaMetrics Item |
| * |
| * Creates a Byte String and sends to the MediaMetrics service. |
| * The Byte String serves as a compact form for logging data |
| * with low overhead for storage. |
| * |
| * The Byte String format is as follows: |
| * |
| * For Java |
| * int64 corresponds to long |
| * int32, uint32 corresponds to int |
| * uint16 corresponds to char |
| * uint8, int8 corresponds to byte |
| * |
| * For items transmitted from Java, uint8 and uint32 values are limited |
| * to INT8_MAX and INT32_MAX. This constrains the size of large items |
| * to 2GB, which is consistent with ByteBuffer max size. A native item |
| * can conceivably have size of 4GB. |
| * |
| * Physical layout of integers and doubles within the MediaMetrics byte string |
| * is in Native / host order, which is usually little endian. |
| * |
| * Note that primitive data (ints, doubles) within a Byte String has |
| * no extra padding or alignment requirements, like ByteBuffer. |
| * |
| * -- begin of item |
| * -- begin of header |
| * (uint32) item size: including the item size field |
| * (uint32) header size, including the item size and header size fields. |
| * (uint16) version: exactly 0 |
| * (uint16) key size, that is key strlen + 1 for zero termination. |
| * (int8)+ key, a string which is 0 terminated (UTF-8). |
| * (int32) pid |
| * (int32) uid |
| * (int64) timestamp |
| * -- end of header |
| * -- begin body |
| * (uint32) number of properties |
| * -- repeat for number of properties |
| * (uint16) property size, including property size field itself |
| * (uint8) type of property |
| * (int8)+ key string, including 0 termination |
| * based on type of property (given above), one of: |
| * (int32) |
| * (int64) |
| * (double) |
| * (int8)+ for TYPE_CSTRING, including 0 termination |
| * (int64, int64) for rate |
| * -- end body |
| * -- end of item |
| * |
| * To record a MediaMetrics event, one creates a new item with an id, |
| * then use a series of puts to add properties |
| * and then a record() to send to the MediaMetrics service. |
| * |
| * The properties may not be unique, and putting a later property with |
| * the same name as an earlier property will overwrite the value and type |
| * of the prior property. |
| * |
| * The timestamp can only be recorded by a system service (and is ignored otherwise; |
| * the MediaMetrics service will fill in the timestamp as needed). |
| * |
| * The units of time are in SystemClock.elapsedRealtimeNanos(). |
| * |
| * A clear() may be called to reset the properties to empty, the time to 0, but keep |
| * the other entries the same. This may be called after record(). |
| * Additional properties may be added after calling record(). Changing the same property |
| * repeatedly is discouraged as - for this particular implementation - extra data |
| * is stored per change. |
| * |
| * new MediaMetrics.Item(mSomeId) |
| * .putString("event", "javaCreate") |
| * .putInt("value", intValue) |
| * .record(); |
| */ |
| |
| /** |
| * Creates an Item with server added uid, time. |
| * |
| * This is the typical way to record a MediaMetrics item. |
| * |
| * @param key the Metrics ID associated with the item. |
| */ |
| public Item(String key) { |
| this(key, -1 /* pid */, -1 /* uid */, 0 /* SystemClock.elapsedRealtimeNanos() */, |
| 2048 /* capacity */); |
| } |
| |
| /** |
| * Creates an Item specifying pid, uid, time, and initial Item capacity. |
| * |
| * This might be used by a service to specify a different PID or UID for a client. |
| * |
| * @param key the Metrics ID associated with the item. |
| * An app may only set properties on an item which has already been |
| * logged previously by a service. |
| * @param pid the process ID corresponding to the item. |
| * A value of -1 (or a record() from an app instead of a service) causes |
| * the MediaMetrics service to fill this in. |
| * @param uid the user ID corresponding to the item. |
| * A value of -1 (or a record() from an app instead of a service) causes |
| * the MediaMetrics service to fill this in. |
| * @param timeNs the time when the item occurred (may be in the past). |
| * A value of 0 (or a record() from an app instead of a service) causes |
| * the MediaMetrics service to fill it in. |
| * Should be obtained from SystemClock.elapsedRealtimeNanos(). |
| * @param capacity the anticipated size to use for the buffer. |
| * If the capacity is too small, the buffer will be resized to accommodate. |
| * This is amortized to copy data no more than twice. |
| */ |
| public Item(String key, int pid, int uid, long timeNs, int capacity) { |
| final byte[] keyBytes = key.getBytes(MEDIAMETRICS_CHARSET); |
| final int keyLength = keyBytes.length; |
| if (keyLength > Character.MAX_VALUE - 1) { |
| throw new IllegalArgumentException("Key length too large"); |
| } |
| |
| // Version 0 - compute the header offsets here. |
| mHeaderSize = 4 + 4 + 2 + 2 + keyLength + 1 + 4 + 4 + 8; // see format above. |
| mPidOffset = mHeaderSize - 16; |
| mUidOffset = mHeaderSize - 12; |
| mTimeNsOffset = mHeaderSize - 8; |
| mPropertyCountOffset = mHeaderSize; |
| mPropertyStartOffset = mHeaderSize + 4; |
| |
| mKey = key; |
| mBuffer = ByteBuffer.allocateDirect( |
| Math.max(capacity, mHeaderSize + MINIMUM_PAYLOAD_SIZE)); |
| |
| // Version 0 - fill the ByteBuffer with the header (some details updated later). |
| mBuffer.order(ByteOrder.nativeOrder()) |
| .putInt((int) 0) // total size in bytes (filled in later) |
| .putInt((int) mHeaderSize) // size of header |
| .putChar((char) FORMAT_VERSION) // version |
| .putChar((char) (keyLength + 1)) // length, with zero termination |
| .put(keyBytes).put((byte) 0) |
| .putInt(pid) |
| .putInt(uid) |
| .putLong(timeNs); |
| if (mHeaderSize != mBuffer.position()) { |
| throw new IllegalStateException("Mismatched sizing"); |
| } |
| mBuffer.putInt(0); // number of properties (to be later filled in by record()). |
| } |
| |
| /** |
| * Sets a metrics typed key |
| * @param key |
| * @param value |
| * @param <T> |
| * @return |
| */ |
| @NonNull |
| public <T> Item set(@NonNull Key<T> key, @Nullable T value) { |
| if (value instanceof Integer) { |
| putInt(key.getName(), (int) value); |
| } else if (value instanceof Long) { |
| putLong(key.getName(), (long) value); |
| } else if (value instanceof Double) { |
| putDouble(key.getName(), (double) value); |
| } else if (value instanceof String) { |
| putString(key.getName(), (String) value); |
| } |
| // if value is null, etc. no error is raised. |
| return this; |
| } |
| |
| /** |
| * Sets the property with key to an integer (32 bit) value. |
| * |
| * @param key |
| * @param value |
| * @return itself |
| */ |
| public Item putInt(String key, int value) { |
| final byte[] keyBytes = key.getBytes(MEDIAMETRICS_CHARSET); |
| final char propSize = (char) reserveProperty(keyBytes, 4 /* payloadSize */); |
| final int estimatedFinalPosition = mBuffer.position() + propSize; |
| mBuffer.putChar(propSize) |
| .put((byte) TYPE_INT32) |
| .put(keyBytes).put((byte) 0) // key, zero terminated |
| .putInt(value); |
| ++mPropertyCount; |
| if (mBuffer.position() != estimatedFinalPosition) { |
| throw new IllegalStateException("Final position " + mBuffer.position() |
| + " != estimatedFinalPosition " + estimatedFinalPosition); |
| } |
| return this; |
| } |
| |
| /** |
| * Sets the property with key to a long (64 bit) value. |
| * |
| * @param key |
| * @param value |
| * @return itself |
| */ |
| public Item putLong(String key, long value) { |
| final byte[] keyBytes = key.getBytes(MEDIAMETRICS_CHARSET); |
| final char propSize = (char) reserveProperty(keyBytes, 8 /* payloadSize */); |
| final int estimatedFinalPosition = mBuffer.position() + propSize; |
| mBuffer.putChar(propSize) |
| .put((byte) TYPE_INT64) |
| .put(keyBytes).put((byte) 0) // key, zero terminated |
| .putLong(value); |
| ++mPropertyCount; |
| if (mBuffer.position() != estimatedFinalPosition) { |
| throw new IllegalStateException("Final position " + mBuffer.position() |
| + " != estimatedFinalPosition " + estimatedFinalPosition); |
| } |
| return this; |
| } |
| |
| /** |
| * Sets the property with key to a double value. |
| * |
| * @param key |
| * @param value |
| * @return itself |
| */ |
| public Item putDouble(String key, double value) { |
| final byte[] keyBytes = key.getBytes(MEDIAMETRICS_CHARSET); |
| final char propSize = (char) reserveProperty(keyBytes, 8 /* payloadSize */); |
| final int estimatedFinalPosition = mBuffer.position() + propSize; |
| mBuffer.putChar(propSize) |
| .put((byte) TYPE_DOUBLE) |
| .put(keyBytes).put((byte) 0) // key, zero terminated |
| .putDouble(value); |
| ++mPropertyCount; |
| if (mBuffer.position() != estimatedFinalPosition) { |
| throw new IllegalStateException("Final position " + mBuffer.position() |
| + " != estimatedFinalPosition " + estimatedFinalPosition); |
| } |
| return this; |
| } |
| |
| /** |
| * Sets the property with key to a String value. |
| * |
| * @param key |
| * @param value |
| * @return itself |
| */ |
| public Item putString(String key, String value) { |
| final byte[] keyBytes = key.getBytes(MEDIAMETRICS_CHARSET); |
| final byte[] valueBytes = value.getBytes(MEDIAMETRICS_CHARSET); |
| final char propSize = (char) reserveProperty(keyBytes, valueBytes.length + 1); |
| final int estimatedFinalPosition = mBuffer.position() + propSize; |
| mBuffer.putChar(propSize) |
| .put((byte) TYPE_CSTRING) |
| .put(keyBytes).put((byte) 0) // key, zero terminated |
| .put(valueBytes).put((byte) 0); // value, zero term. |
| ++mPropertyCount; |
| if (mBuffer.position() != estimatedFinalPosition) { |
| throw new IllegalStateException("Final position " + mBuffer.position() |
| + " != estimatedFinalPosition " + estimatedFinalPosition); |
| } |
| return this; |
| } |
| |
| /** |
| * Sets the pid to the provided value. |
| * |
| * @param pid which can be -1 if the service is to fill it in from the calling info. |
| * @return itself |
| */ |
| public Item setPid(int pid) { |
| mBuffer.putInt(mPidOffset, pid); // pid location in byte string. |
| return this; |
| } |
| |
| /** |
| * Sets the uid to the provided value. |
| * |
| * The UID represents the client associated with the property. This must be the UID |
| * of the application if it comes from the application client. |
| * |
| * Trusted services are allowed to set the uid for a client-related item. |
| * |
| * @param uid which can be -1 if the service is to fill it in from calling info. |
| * @return itself |
| */ |
| public Item setUid(int uid) { |
| mBuffer.putInt(mUidOffset, uid); // uid location in byte string. |
| return this; |
| } |
| |
| /** |
| * Sets the timestamp to the provided value. |
| * |
| * The time is referenced by the Boottime obtained by SystemClock.elapsedRealtimeNanos(). |
| * This should be associated with the occurrence of the event. It is recommended that |
| * the event be registered immediately when it occurs, and no later than 500ms |
| * (and certainly not in the future). |
| * |
| * @param timeNs which can be 0 if the service is to fill it in at the time of call. |
| * @return itself |
| */ |
| public Item setTimestamp(long timeNs) { |
| mBuffer.putLong(mTimeNsOffset, timeNs); // time location in byte string. |
| return this; |
| } |
| |
| /** |
| * Clears the properties and resets the time to 0. |
| * |
| * No other values are changed. |
| * |
| * @return itself |
| */ |
| public Item clear() { |
| mBuffer.position(mPropertyStartOffset); |
| mBuffer.limit(mBuffer.capacity()); |
| mBuffer.putLong(mTimeNsOffset, 0); // reset time. |
| mPropertyCount = 0; |
| return this; |
| } |
| |
| /** |
| * Sends the item to the MediaMetrics service. |
| * |
| * The item properties are unchanged, hence record() may be called more than once |
| * to send the same item twice. Also, record() may be called without any properties. |
| * |
| * @return true if successful. |
| */ |
| public boolean record() { |
| updateHeader(); |
| return native_submit_bytebuffer(mBuffer, mBuffer.limit()) >= 0; |
| } |
| |
| /** |
| * Converts the Item to a Bundle. |
| * |
| * This is primarily used as a test API for CTS. |
| * |
| * @return a Bundle with the keys set according to data in the Item's buffer. |
| */ |
| public Bundle toBundle() { |
| updateHeader(); |
| |
| final ByteBuffer buffer = mBuffer.duplicate(); |
| buffer.order(ByteOrder.nativeOrder()) // restore order property |
| .flip(); // convert from write buffer to read buffer |
| |
| return toBundle(buffer); |
| } |
| |
| // The following constants are used for tests to extract |
| // the content of the Bundle for CTS testing. |
| public static final String BUNDLE_TOTAL_SIZE = "_totalSize"; |
| public static final String BUNDLE_HEADER_SIZE = "_headerSize"; |
| public static final String BUNDLE_VERSION = "_version"; |
| public static final String BUNDLE_KEY_SIZE = "_keySize"; |
| public static final String BUNDLE_KEY = "_key"; |
| public static final String BUNDLE_PID = "_pid"; |
| public static final String BUNDLE_UID = "_uid"; |
| public static final String BUNDLE_TIMESTAMP = "_timestamp"; |
| public static final String BUNDLE_PROPERTY_COUNT = "_propertyCount"; |
| |
| /** |
| * Converts a buffer contents to a bundle |
| * |
| * This is primarily used as a test API for CTS. |
| * |
| * @param buffer contains the byte data serialized according to the byte string version. |
| * @return a Bundle with the keys set according to data in the buffer. |
| */ |
| public static Bundle toBundle(ByteBuffer buffer) { |
| final Bundle bundle = new Bundle(); |
| |
| final int totalSize = buffer.getInt(); |
| final int headerSize = buffer.getInt(); |
| final char version = buffer.getChar(); |
| final char keySize = buffer.getChar(); // includes zero termination, i.e. keyLength + 1 |
| |
| if (totalSize < 0 || headerSize < 0) { |
| throw new IllegalArgumentException("Item size cannot be > " + Integer.MAX_VALUE); |
| } |
| final String key; |
| if (keySize > 0) { |
| key = getStringFromBuffer(buffer, keySize); |
| } else { |
| throw new IllegalArgumentException("Illegal null key"); |
| } |
| |
| final int pid = buffer.getInt(); |
| final int uid = buffer.getInt(); |
| final long timestamp = buffer.getLong(); |
| |
| // Verify header size (depending on version). |
| final int headerRead = buffer.position(); |
| if (version == 0) { |
| if (headerRead != headerSize) { |
| throw new IllegalArgumentException( |
| "Item key:" + key |
| + " headerRead:" + headerRead + " != headerSize:" + headerSize); |
| } |
| } else { |
| // future versions should only increase header size |
| // by adding to the end. |
| if (headerRead > headerSize) { |
| throw new IllegalArgumentException( |
| "Item key:" + key |
| + " headerRead:" + headerRead + " > headerSize:" + headerSize); |
| } else if (headerRead < headerSize) { |
| buffer.position(headerSize); |
| } |
| } |
| |
| // Body always starts with properties. |
| final int propertyCount = buffer.getInt(); |
| if (propertyCount < 0) { |
| throw new IllegalArgumentException( |
| "Cannot have more than " + Integer.MAX_VALUE + " properties"); |
| } |
| bundle.putInt(BUNDLE_TOTAL_SIZE, totalSize); |
| bundle.putInt(BUNDLE_HEADER_SIZE, headerSize); |
| bundle.putChar(BUNDLE_VERSION, version); |
| bundle.putChar(BUNDLE_KEY_SIZE, keySize); |
| bundle.putString(BUNDLE_KEY, key); |
| bundle.putInt(BUNDLE_PID, pid); |
| bundle.putInt(BUNDLE_UID, uid); |
| bundle.putLong(BUNDLE_TIMESTAMP, timestamp); |
| bundle.putInt(BUNDLE_PROPERTY_COUNT, propertyCount); |
| |
| for (int i = 0; i < propertyCount; ++i) { |
| final int initialBufferPosition = buffer.position(); |
| final char propSize = buffer.getChar(); |
| final byte type = buffer.get(); |
| |
| // Log.d(TAG, "(" + i + ") propSize:" + ((int)propSize) + " type:" + type); |
| final String propKey = getStringFromBuffer(buffer); |
| switch (type) { |
| case TYPE_INT32: |
| bundle.putInt(propKey, buffer.getInt()); |
| break; |
| case TYPE_INT64: |
| bundle.putLong(propKey, buffer.getLong()); |
| break; |
| case TYPE_DOUBLE: |
| bundle.putDouble(propKey, buffer.getDouble()); |
| break; |
| case TYPE_CSTRING: |
| bundle.putString(propKey, getStringFromBuffer(buffer)); |
| break; |
| case TYPE_NONE: |
| break; // ignore on Java side |
| case TYPE_RATE: |
| buffer.getLong(); // consume the first int64_t of rate |
| buffer.getLong(); // consume the second int64_t of rate |
| break; // ignore on Java side |
| default: |
| // These are unsupported types for version 0 |
| // We ignore them if the version is greater than 0. |
| if (version == 0) { |
| throw new IllegalArgumentException( |
| "Property " + propKey + " has unsupported type " + type); |
| } |
| buffer.position(initialBufferPosition + propSize); // advance and skip |
| break; |
| } |
| final int deltaPosition = buffer.position() - initialBufferPosition; |
| if (deltaPosition != propSize) { |
| throw new IllegalArgumentException("propSize:" + propSize |
| + " != deltaPosition:" + deltaPosition); |
| } |
| } |
| |
| final int finalPosition = buffer.position(); |
| if (finalPosition != totalSize) { |
| throw new IllegalArgumentException("totalSize:" + totalSize |
| + " != finalPosition:" + finalPosition); |
| } |
| return bundle; |
| } |
| |
| // Version 0 byte offsets for the header. |
| private static final int FORMAT_VERSION = 0; |
| private static final int TOTAL_SIZE_OFFSET = 0; |
| private static final int HEADER_SIZE_OFFSET = 4; |
| private static final int MINIMUM_PAYLOAD_SIZE = 4; |
| private final int mPidOffset; // computed in constructor |
| private final int mUidOffset; // computed in constructor |
| private final int mTimeNsOffset; // computed in constructor |
| private final int mPropertyCountOffset; // computed in constructor |
| private final int mPropertyStartOffset; // computed in constructor |
| private final int mHeaderSize; // computed in constructor |
| |
| private final String mKey; |
| |
| private ByteBuffer mBuffer; // may be reallocated if capacity is insufficient. |
| private int mPropertyCount = 0; // overflow not checked (mBuffer would overflow first). |
| |
| private int reserveProperty(byte[] keyBytes, int payloadSize) { |
| final int keyLength = keyBytes.length; |
| if (keyLength > Character.MAX_VALUE) { |
| throw new IllegalStateException("property key too long " |
| + new String(keyBytes, MEDIAMETRICS_CHARSET)); |
| } |
| if (payloadSize > Character.MAX_VALUE) { |
| throw new IllegalStateException("payload too large " + payloadSize); |
| } |
| |
| // See the byte string property format above. |
| final int size = 2 /* length */ |
| + 1 /* type */ |
| + keyLength + 1 /* key length with zero termination */ |
| + payloadSize; /* payload size */ |
| |
| if (size > Character.MAX_VALUE) { |
| throw new IllegalStateException("Item property " |
| + new String(keyBytes, MEDIAMETRICS_CHARSET) + " is too large to send"); |
| } |
| |
| if (mBuffer.remaining() < size) { |
| int newCapacity = mBuffer.position() + size; |
| if (newCapacity > Integer.MAX_VALUE >> 1) { |
| throw new IllegalStateException( |
| "Item memory requirements too large: " + newCapacity); |
| } |
| newCapacity <<= 1; |
| ByteBuffer buffer = ByteBuffer.allocateDirect(newCapacity); |
| buffer.order(ByteOrder.nativeOrder()); |
| |
| // Copy data from old buffer to new buffer. |
| mBuffer.flip(); |
| buffer.put(mBuffer); |
| |
| // set buffer to new buffer |
| mBuffer = buffer; |
| } |
| return size; |
| } |
| |
| // Used for test |
| private static String getStringFromBuffer(ByteBuffer buffer) { |
| return getStringFromBuffer(buffer, Integer.MAX_VALUE); |
| } |
| |
| // Used for test |
| private static String getStringFromBuffer(ByteBuffer buffer, int size) { |
| int i = buffer.position(); |
| int limit = buffer.limit(); |
| if (size < Integer.MAX_VALUE - i && i + size < limit) { |
| limit = i + size; |
| } |
| for (; i < limit; ++i) { |
| if (buffer.get(i) == 0) { |
| final int newPosition = i + 1; |
| if (size != Integer.MAX_VALUE && newPosition - buffer.position() != size) { |
| throw new IllegalArgumentException("chars consumed at " + i + ": " |
| + (newPosition - buffer.position()) + " != size: " + size); |
| } |
| final String found; |
| if (buffer.hasArray()) { |
| found = new String( |
| buffer.array(), buffer.position() + buffer.arrayOffset(), |
| i - buffer.position(), MEDIAMETRICS_CHARSET); |
| buffer.position(newPosition); |
| } else { |
| final byte[] array = new byte[i - buffer.position()]; |
| buffer.get(array); |
| found = new String(array, MEDIAMETRICS_CHARSET); |
| buffer.get(); // remove 0. |
| } |
| return found; |
| } |
| } |
| throw new IllegalArgumentException( |
| "No zero termination found in string position: " |
| + buffer.position() + " end: " + i); |
| } |
| |
| /** |
| * May be called multiple times - just makes the header consistent with the current |
| * properties written. |
| */ |
| private void updateHeader() { |
| // Buffer sized properly in constructor. |
| mBuffer.putInt(TOTAL_SIZE_OFFSET, mBuffer.position()) // set total length |
| .putInt(mPropertyCountOffset, (char) mPropertyCount); // set number of properties |
| } |
| } |
| |
| private static native int native_submit_bytebuffer(@NonNull ByteBuffer buffer, int length); |
| } |