Merge "MediaMetrics: Add MediaMetrics Java interface"
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index f7a994f..3cde887 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -129,6 +129,7 @@
 extern int register_android_database_SQLiteConnection(JNIEnv* env);
 extern int register_android_database_SQLiteGlobal(JNIEnv* env);
 extern int register_android_database_SQLiteDebug(JNIEnv* env);
+extern int register_android_media_MediaMetrics(JNIEnv *env);
 extern int register_android_os_Debug(JNIEnv* env);
 extern int register_android_os_GraphicsEnvironment(JNIEnv* env);
 extern int register_android_os_HidlSupport(JNIEnv* env);
@@ -1520,6 +1521,7 @@
     REG_JNI(register_android_media_AudioProductStrategies),
     REG_JNI(register_android_media_AudioVolumeGroups),
     REG_JNI(register_android_media_AudioVolumeGroupChangeHandler),
+    REG_JNI(register_android_media_MediaMetrics),
     REG_JNI(register_android_media_MicrophoneInfo),
     REG_JNI(register_android_media_RemoteDisplay),
     REG_JNI(register_android_media_ToneGenerator),
diff --git a/media/java/android/media/MediaMetrics.java b/media/java/android/media/MediaMetrics.java
new file mode 100644
index 0000000..88a8295
--- /dev/null
+++ b/media/java/android/media/MediaMetrics.java
@@ -0,0 +1,634 @@
+/*
+ * 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.TestApi;
+import android.os.Bundle;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 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";
+
+    /**
+     * 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;
+
+    /**
+     * 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 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.
+         */
+        @TestApi
+        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.
+        @TestApi
+        public static final String BUNDLE_TOTAL_SIZE = "_totalSize";
+        @TestApi
+        public static final String BUNDLE_HEADER_SIZE = "_headerSize";
+        @TestApi
+        public static final String BUNDLE_VERSION = "_version";
+        @TestApi
+        public static final String BUNDLE_KEY_SIZE = "_keySize";
+        @TestApi
+        public static final String BUNDLE_KEY = "_key";
+        @TestApi
+        public static final String BUNDLE_PID = "_pid";
+        @TestApi
+        public static final String BUNDLE_UID = "_uid";
+        @TestApi
+        public static final String BUNDLE_TIMESTAMP = "_timestamp";
+        @TestApi
+        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.
+         */
+        @TestApi
+        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);
+}
diff --git a/media/jni/android_media_MediaMetricsJNI.cpp b/media/jni/android_media_MediaMetricsJNI.cpp
index 494c617..e17a617 100644
--- a/media/jni/android_media_MediaMetricsJNI.cpp
+++ b/media/jni/android_media_MediaMetricsJNI.cpp
@@ -23,6 +23,7 @@
 
 #include "android_media_MediaMetricsJNI.h"
 #include "android_os_Parcel.h"
+#include "android_runtime/AndroidRuntime.h"
 
 // This source file is compiled and linked into:
 // core/jni/ (libandroid_runtime.so)
@@ -124,6 +125,28 @@
     return bh.bundle;
 }
 
+// Implementation of MediaMetrics.native_submit_bytebuffer(),
+// Delivers the byte buffer to the mediametrics service.
+static jint android_media_MediaMetrics_submit_bytebuffer(
+        JNIEnv* env, jobject thiz, jobject byteBuffer, jint length)
+{
+    const jbyte* buffer =
+            reinterpret_cast<const jbyte*>(env->GetDirectBufferAddress(byteBuffer));
+    if (buffer == nullptr) {
+        ALOGE("Error retrieving source of audio data to play, can't play");
+        return (jint)BAD_VALUE;
+    }
+
+    // TODO: directly record item to MediaMetrics service.
+    mediametrics::Item item;
+    if (item.readFromByteString((char *)buffer, length) != NO_ERROR) {
+        ALOGW("%s: cannot read from byte string", __func__);
+        return (jint)BAD_VALUE;
+    }
+    item.selfrecord();
+    return (jint)NO_ERROR;
+}
+
 // Helper function to convert a native PersistableBundle to a Java
 // PersistableBundle.
 jobject MediaMetricsJNI::nativeToJavaPersistableBundle(JNIEnv *env,
@@ -191,5 +214,18 @@
     return newBundle;
 }
 
-};  // namespace android
+// ----------------------------------------------------------------------------
 
+static constexpr JNINativeMethod gMethods[] = {
+    {"native_submit_bytebuffer", "(Ljava/nio/ByteBuffer;I)I",
+            (void *)android_media_MediaMetrics_submit_bytebuffer},
+};
+
+// Registers the native methods, called from core/jni/AndroidRuntime.cpp
+int register_android_media_MediaMetrics(JNIEnv *env)
+{
+    return AndroidRuntime::registerNativeMethods(
+            env, "android/media/MediaMetrics", gMethods, std::size(gMethods));
+}
+
+};  // namespace android