Custom binary XML wire protocol.

We've identified that XML writing and reading uses roughly 1.5% of
all system_server CPU, and can generate many temporary objects.

Building on the recent TypedXmlSerializer/PullParser interfaces, this
change introduces new BinaryXmlSerializer/PullParser implementations
that store data using a custom binary wire protocol.  Benchmarking of
a typical packages.xml has shown this new binary approach can write
4.3x faster and read 8.5x faster, while using 2.4x less disk space:

    timeWrite_Fast_mean: 27946635
    timeWrite_Binary_mean: 6519341

    timeRead_Fast_mean: 59562531
    timeRead_Binary_mean: 7020185

A major factor in choosing to invest in this new wire protocol is
that it enables the long-tail of over 100 unique XML schemas used
across the OS internals to be transparently upgraded to gain these
benefits with only minimal changes, reducing the risks associated
with rewriting those schemas.

Finally, since the wire protocol is essentially a serialized event
stream, it's trivial to transparently convert this new protocol
into human-readable XML and vice-versa.  The tests in this change
demonstrate this translation working correctly, and future changes
will introduce new shell tools to aid development work.

Bug: 171832118
Test: atest FrameworksCoreTests:android.util.XmlTest
Test: atest FrameworksCoreTests:android.util.BinaryXmlTest
Test: atest CorePerfTests:android.util.XmlPerfTest
Change-Id: Ib9390701f09562dca952b3786622675b9c68a462
diff --git a/apct-tests/perftests/core/src/android/util/XmlPerfTest.java b/apct-tests/perftests/core/src/android/util/XmlPerfTest.java
new file mode 100644
index 0000000..e05bd2a
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/util/XmlPerfTest.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2020 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 org.junit.Assert.assertEquals;
+
+import android.os.Bundle;
+import android.os.Debug;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.HexDump;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Supplier;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class XmlPerfTest {
+    /**
+     * Since allocation measurement adds overhead, it's disabled by default for
+     * performance runs. It can be manually enabled to compare GC behavior.
+     */
+    private static final boolean MEASURE_ALLOC = false;
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void timeWrite_Fast() throws Exception {
+        doWrite(() -> Xml.newFastSerializer());
+    }
+
+    @Test
+    public void timeWrite_Binary() throws Exception {
+        doWrite(() -> Xml.newBinarySerializer());
+    }
+
+    private void doWrite(Supplier<TypedXmlSerializer> outFactory) throws Exception {
+        if (MEASURE_ALLOC) {
+            Debug.startAllocCounting();
+        }
+
+        int iterations = 0;
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            iterations++;
+            try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+                final TypedXmlSerializer out = outFactory.get();
+                out.setOutput(os, StandardCharsets.UTF_8.name());
+                write(out);
+            }
+        }
+
+        if (MEASURE_ALLOC) {
+            Debug.stopAllocCounting();
+            final Bundle results = new Bundle();
+            results.putLong("threadAllocCount_mean", Debug.getThreadAllocCount() / iterations);
+            results.putLong("threadAllocSize_mean", Debug.getThreadAllocSize() / iterations);
+            InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
+        }
+    }
+
+    @Test
+    public void timeRead_Fast() throws Exception {
+        doRead(() -> Xml.newFastSerializer(), () -> Xml.newFastPullParser());
+    }
+
+    @Test
+    public void timeRead_Binary() throws Exception {
+        doRead(() -> Xml.newBinarySerializer(), () -> Xml.newBinaryPullParser());
+    }
+
+    private void doRead(Supplier<TypedXmlSerializer> outFactory,
+            Supplier<TypedXmlPullParser> inFactory) throws Exception {
+        final byte[] raw;
+        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+            TypedXmlSerializer out = outFactory.get();
+            out.setOutput(os, StandardCharsets.UTF_8.name());
+            write(out);
+            raw = os.toByteArray();
+        }
+
+        if (MEASURE_ALLOC) {
+            Debug.startAllocCounting();
+        }
+
+        int iterations = 0;
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            iterations++;
+            try (ByteArrayInputStream is = new ByteArrayInputStream(raw)) {
+                TypedXmlPullParser xml = inFactory.get();
+                xml.setInput(is, StandardCharsets.UTF_8.name());
+                read(xml);
+            }
+        }
+
+        if (MEASURE_ALLOC) {
+            Debug.stopAllocCounting();
+            final Bundle results = new Bundle();
+            results.putLong("sizeBytes", raw.length);
+            results.putLong("threadAllocCount_mean", Debug.getThreadAllocCount() / iterations);
+            results.putLong("threadAllocSize_mean", Debug.getThreadAllocSize() / iterations);
+            InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
+        } else {
+            final Bundle results = new Bundle();
+            results.putLong("sizeBytes", raw.length);
+            InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
+        }
+    }
+
+    /**
+     * Not even joking, this is a typical public key blob stored in
+     * {@code packages.xml}.
+     */
+    private static final byte[] KEY_BLOB = HexDump.hexStringToByteArray(""
+            + "308204a830820390a003020102020900a1573d0f45bea193300d06092a864886f70d010105050030819"
+            + "4310b3009060355040613025553311330110603550408130a43616c69666f726e696131163014060355"
+            + "0407130d4d6f756e7461696e20566965773110300e060355040a1307416e64726f69643110300e06035"
+            + "5040b1307416e64726f69643110300e06035504031307416e64726f69643122302006092a864886f70d"
+            + "0109011613616e64726f696440616e64726f69642e636f6d301e170d3131303931393138343232355a1"
+            + "70d3339303230343138343232355a308194310b3009060355040613025553311330110603550408130a"
+            + "43616c69666f726e6961311630140603550407130d4d6f756e7461696e20566965773110300e0603550"
+            + "40a1307416e64726f69643110300e060355040b1307416e64726f69643110300e06035504031307416e"
+            + "64726f69643122302006092a864886f70d0109011613616e64726f696440616e64726f69642e636f6d3"
+            + "0820120300d06092a864886f70d01010105000382010d00308201080282010100de1b51336afc909d8b"
+            + "cca5920fcdc8940578ec5c253898930e985481cfdea75ba6fc54b1f7bb492a03d98db471ab4200103a8"
+            + "314e60ee25fef6c8b83bc1b2b45b084874cffef148fa2001bb25c672b6beba50b7ac026b546da762ea2"
+            + "23829a22b80ef286131f059d2c9b4ca71d54e515a8a3fd6bf5f12a2493dfc2619b337b032a7cf8bbd34"
+            + "b833f2b93aeab3d325549a93272093943bb59dfc0197ae4861ff514e019b73f5cf10023ad1a032adb4b"
+            + "9bbaeb4debecb4941d6a02381f1165e1ac884c1fca9525c5854dce2ad8ec839b8ce78442c16367efc07"
+            + "778a337d3ca2cdf9792ac722b95d67c345f1c00976ec372f02bfcbef0262cc512a6845e71cfea0d0201"
+            + "03a381fc3081f9301d0603551d0e0416041478a0fc4517fb70ff52210df33c8d32290a44b2bb3081c90"
+            + "603551d230481c13081be801478a0fc4517fb70ff52210df33c8d32290a44b2bba1819aa48197308194"
+            + "310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550"
+            + "407130d4d6f756e7461696e20566965773110300e060355040a1307416e64726f69643110300e060355"
+            + "040b1307416e64726f69643110300e06035504031307416e64726f69643122302006092a864886f70d0"
+            + "109011613616e64726f696440616e64726f69642e636f6d820900a1573d0f45bea193300c0603551d13"
+            + "040530030101ff300d06092a864886f70d01010505000382010100977302dfbf668d7c61841c9c78d25"
+            + "63bcda1b199e95e6275a799939981416909722713531157f3cdcfea94eea7bb79ca3ca972bd8058a36a"
+            + "d1919291df42d7190678d4ea47a4b9552c9dfb260e6d0d9129b44615cd641c1080580e8a990dd768c6a"
+            + "b500c3b964e185874e4105109d94c5bd8c405deb3cf0f7960a563bfab58169a956372167a7e2674a04c"
+            + "4f80015d8f7869a7a4139aecbbdca2abc294144ee01e4109f0e47a518363cf6e9bf41f7560e94bdd4a5"
+            + "d085234796b05c7a1389adfd489feec2a107955129d7991daa49afb3d327dc0dc4fe959789372b093a8"
+            + "9c8dbfa41554f771c18015a6cb242a17e04d19d55d3b4664eae12caf2a11cd2b836e");
+
+    /**
+     * Typical list of permissions referenced in {@code packages.xml}.
+     */
+    private static final String[] PERMS = new String[] {
+            "android.permission.ACCESS_CACHE_FILESYSTEM",
+            "android.permission.WRITE_SETTINGS",
+            "android.permission.MANAGE_EXTERNAL_STORAGE",
+            "android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS",
+            "android.permission.FOREGROUND_SERVICE",
+            "android.permission.RECEIVE_BOOT_COMPLETED",
+            "android.permission.WRITE_MEDIA_STORAGE",
+            "android.permission.INTERNET",
+            "android.permission.UPDATE_DEVICE_STATS",
+            "android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY",
+            "android.permission.MANAGE_USB",
+            "android.permission.ACCESS_ALL_DOWNLOADS",
+            "android.permission.ACCESS_DOWNLOAD_MANAGER",
+            "android.permission.MANAGE_USERS",
+            "android.permission.ACCESS_NETWORK_STATE",
+            "android.permission.ACCESS_MTP",
+            "android.permission.INTERACT_ACROSS_USERS",
+            "android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS",
+            "android.permission.CLEAR_APP_CACHE",
+            "android.permission.CONNECTIVITY_INTERNAL",
+            "android.permission.START_ACTIVITIES_FROM_BACKGROUND",
+            "android.permission.QUERY_ALL_PACKAGES",
+            "android.permission.WAKE_LOCK",
+            "android.permission.UPDATE_APP_OPS_STATS",
+    };
+
+    /**
+     * Write a typical {@code packages.xml} file containing 100 applications,
+     * each of which defines signing key and permission information.
+     */
+    private static void write(TypedXmlSerializer out) throws IOException {
+        out.startDocument(null, true);
+        out.startTag(null, "packages");
+        for (int i = 0; i < 100; i++) {
+            out.startTag(null, "package");
+            out.attribute(null, "name", "com.android.providers.media");
+            out.attribute(null, "codePath", "/system/priv-app/MediaProviderLegacy");
+            out.attribute(null, "nativeLibraryPath", "/system/priv-app/MediaProviderLegacy/lib");
+            out.attributeLong(null, "publicFlags", 944258629L);
+            out.attributeLong(null, "privateFlags", -1946152952L);
+            out.attributeLong(null, "ft", 1603899064000L);
+            out.attributeLong(null, "it", 1603899064000L);
+            out.attributeLong(null, "ut", 1603899064000L);
+            out.attributeInt(null, "version", 1024);
+            out.attributeInt(null, "sharedUserId", 10100);
+            out.attributeBoolean(null, "isOrphaned", true);
+
+            out.startTag(null, "sigs");
+            out.startTag(null, "cert");
+            out.attributeInt(null, "index", 10);
+            out.attributeBytesHex(null, "key", KEY_BLOB);
+            out.endTag(null, "cert");
+            out.endTag(null, "sigs");
+
+            out.startTag(null, "perms");
+            for (String perm : PERMS) {
+                out.startTag(null, "item");
+                out.attributeInterned(null, "name", perm);
+                out.attributeBoolean(null, "granted", true);
+                out.attributeInt(null, "flags", 0);
+                out.endTag(null, "item");
+            }
+            out.endTag(null, "perms");
+
+            out.endTag(null, "package");
+        }
+        out.endTag(null, "packages");
+        out.endDocument();
+    }
+
+    /**
+     * Read a typical {@code packages.xml} file containing 100 applications, and
+     * verify that data passes smell test.
+     */
+    private static void read(TypedXmlPullParser xml) throws Exception {
+        int type;
+        int packages = 0;
+        int certs = 0;
+        int perms = 0;
+        while ((type = xml.next()) != XmlPullParser.END_DOCUMENT) {
+            final String tag = xml.getName();
+            if (type == XmlPullParser.START_TAG) {
+                if ("package".equals(tag)) {
+                    xml.getAttributeValue(null, "name");
+                    xml.getAttributeValue(null, "codePath");
+                    xml.getAttributeValue(null, "nativeLibraryPath");
+                    xml.getAttributeLong(null, "publicFlags");
+                    assertEquals(-1946152952L, xml.getAttributeLong(null, "privateFlags"));
+                    xml.getAttributeLong(null, "ft");
+                    xml.getAttributeLong(null, "it");
+                    xml.getAttributeLong(null, "ut");
+                    xml.getAttributeInt(null, "version");
+                    xml.getAttributeInt(null, "sharedUserId");
+                    xml.getAttributeBoolean(null, "isOrphaned");
+                    packages++;
+                } else if ("cert".equals(tag)) {
+                    xml.getAttributeInt(null, "index");
+                    xml.getAttributeBytesHex(null, "key");
+                    certs++;
+                } else if ("item".equals(tag)) {
+                    xml.getAttributeValue(null, "name");
+                    xml.getAttributeBoolean(null, "granted");
+                    xml.getAttributeInt(null, "flags");
+                    perms++;
+                }
+            } else if (type == XmlPullParser.TEXT) {
+                xml.getText();
+            }
+        }
+
+        assertEquals(100, packages);
+        assertEquals(packages * 1, certs);
+        assertEquals(packages * PERMS.length, perms);
+    }
+}
diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java
index 9849c10..cc6ed2e 100644
--- a/core/java/android/util/Xml.java
+++ b/core/java/android/util/Xml.java
@@ -17,7 +17,10 @@
 package android.util;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
+import com.android.internal.util.BinaryXmlPullParser;
+import com.android.internal.util.BinaryXmlSerializer;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.XmlUtils;
 
@@ -121,6 +124,18 @@
     }
 
     /**
+     * Creates a new {@link XmlPullParser} that reads XML documents using a
+     * custom binary wire protocol which benchmarking has shown to be 8.5x
+     * faster than {@code Xml.newFastPullParser()} for a typical
+     * {@code packages.xml}.
+     *
+     * @hide
+     */
+    public static @NonNull TypedXmlPullParser newBinaryPullParser() {
+        return new BinaryXmlPullParser();
+    }
+
+    /**
      * Creates a new {@link XmlPullParser} which is optimized for use inside the
      * system, typically by supporting only a basic set of features.
      * <p>
@@ -167,6 +182,18 @@
     }
 
     /**
+     * Creates a new {@link XmlSerializer} that writes XML documents using a
+     * custom binary wire protocol which benchmarking has shown to be 4.4x
+     * faster and use 2.8x less disk space than {@code Xml.newFastSerializer()}
+     * for a typical {@code packages.xml}.
+     *
+     * @hide
+     */
+    public static @NonNull TypedXmlSerializer newBinarySerializer() {
+        return new BinaryXmlSerializer();
+    }
+
+    /**
      * Creates a new {@link XmlSerializer} which is optimized for use inside the
      * system, typically by supporting only a basic set of features.
      * <p>
@@ -189,6 +216,82 @@
     }
 
     /**
+     * Copy the first XML document into the second document.
+     * <p>
+     * Implemented by reading all events from the given {@link XmlPullParser}
+     * and writing them directly to the given {@link XmlSerializer}. This can be
+     * useful for transparently converting between underlying wire protocols.
+     *
+     * @hide
+     */
+    public static void copy(@NonNull XmlPullParser in, @NonNull XmlSerializer out)
+            throws XmlPullParserException, IOException {
+        // Some parsers may have already consumed the event that starts the
+        // document, so we manually emit that event here for consistency
+        if (in.getEventType() == XmlPullParser.START_DOCUMENT) {
+            out.startDocument(in.getInputEncoding(), true);
+        }
+
+        while (true) {
+            final int token = in.nextToken();
+            switch (token) {
+                case XmlPullParser.START_DOCUMENT:
+                    out.startDocument(in.getInputEncoding(), true);
+                    break;
+                case XmlPullParser.END_DOCUMENT:
+                    out.endDocument();
+                    return;
+                case XmlPullParser.START_TAG:
+                    out.startTag(normalizeNamespace(in.getNamespace()), in.getName());
+                    for (int i = 0; i < in.getAttributeCount(); i++) {
+                        out.attribute(normalizeNamespace(in.getAttributeNamespace(i)),
+                                in.getAttributeName(i), in.getAttributeValue(i));
+                    }
+                    break;
+                case XmlPullParser.END_TAG:
+                    out.endTag(normalizeNamespace(in.getNamespace()), in.getName());
+                    break;
+                case XmlPullParser.TEXT:
+                    out.text(in.getText());
+                    break;
+                case XmlPullParser.CDSECT:
+                    out.cdsect(in.getText());
+                    break;
+                case XmlPullParser.ENTITY_REF:
+                    out.entityRef(in.getName());
+                    break;
+                case XmlPullParser.IGNORABLE_WHITESPACE:
+                    out.ignorableWhitespace(in.getText());
+                    break;
+                case XmlPullParser.PROCESSING_INSTRUCTION:
+                    out.processingInstruction(in.getText());
+                    break;
+                case XmlPullParser.COMMENT:
+                    out.comment(in.getText());
+                    break;
+                case XmlPullParser.DOCDECL:
+                    out.docdecl(in.getText());
+                    break;
+                default:
+                    throw new IllegalStateException("Unknown token " + token);
+            }
+        }
+    }
+
+    /**
+     * Some parsers may return an empty string {@code ""} when a namespace in
+     * unsupported, which can confuse serializers. This method normalizes empty
+     * strings to be {@code null}.
+     */
+    private static @Nullable String normalizeNamespace(@Nullable String namespace) {
+        if (namespace == null || namespace.isEmpty()) {
+            return null;
+        } else {
+            return namespace;
+        }
+    }
+
+    /**
      * Supported character encodings.
      */
     public enum Encoding {
diff --git a/core/java/com/android/internal/util/BinaryXmlPullParser.java b/core/java/com/android/internal/util/BinaryXmlPullParser.java
new file mode 100644
index 0000000..da16eca
--- /dev/null
+++ b/core/java/com/android/internal/util/BinaryXmlPullParser.java
@@ -0,0 +1,899 @@
+/*
+ * Copyright (C) 2020 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.internal.util;
+
+import static com.android.internal.util.BinaryXmlSerializer.ATTRIBUTE;
+import static com.android.internal.util.BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_BOOLEAN_FALSE;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_BOOLEAN_TRUE;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_BYTES_BASE64;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_BYTES_HEX;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_DOUBLE;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_FLOAT;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_INT;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_INT_HEX;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_LONG;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_LONG_HEX;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_NULL;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_STRING;
+import static com.android.internal.util.BinaryXmlSerializer.TYPE_STRING_INTERNED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.TypedXmlPullParser;
+import android.util.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Parser that reads XML documents using a custom binary wire protocol which
+ * benchmarking has shown to be 8.5x faster than {@link Xml.newFastPullParser()}
+ * for a typical {@code packages.xml}.
+ * <p>
+ * The high-level design of the wire protocol is to directly serialize the event
+ * stream, while efficiently and compactly writing strongly-typed primitives
+ * delivered through the {@link TypedXmlSerializer} interface.
+ * <p>
+ * Each serialized event is a single byte where the lower half is a normal
+ * {@link XmlPullParser} token and the upper half is an optional data type
+ * signal, such as {@link #TYPE_INT}.
+ * <p>
+ * This parser has some specific limitations:
+ * <ul>
+ * <li>Only the UTF-8 encoding is supported.
+ * <li>Variable length values, such as {@code byte[]} or {@link String}, are
+ * limited to 65,535 bytes in length. Note that {@link String} values are stored
+ * as UTF-8 on the wire.
+ * <li>Namespaces, prefixes, properties, and options are unsupported.
+ * </ul>
+ */
+public final class BinaryXmlPullParser implements TypedXmlPullParser {
+    /**
+     * Default buffer size, which matches {@code FastXmlSerializer}. This should
+     * be kept in sync with {@link BinaryXmlPullParser}.
+     */
+    private static final int BUFFER_SIZE = 32_768;
+
+    private FastDataInput mIn;
+
+    private int mCurrentToken = START_DOCUMENT;
+    private int mCurrentDepth = 0;
+    private String mCurrentName;
+    private String mCurrentText;
+
+    /**
+     * Pool of attributes parsed for the currently tag. All interactions should
+     * be done via {@link #obtainAttribute()}, {@link #findAttribute(String)},
+     * and {@link #resetAttributes()}.
+     */
+    private int mAttributeCount = 0;
+    private Attribute[] mAttributes;
+
+    @Override
+    public void setInput(InputStream is, String inputEncoding) throws XmlPullParserException {
+        if (inputEncoding != null && !StandardCharsets.UTF_8.name().equals(inputEncoding)) {
+            throw new UnsupportedOperationException();
+        }
+
+        mIn = new FastDataInput(is, BUFFER_SIZE);
+
+        mCurrentToken = START_DOCUMENT;
+        mCurrentDepth = 0;
+        mCurrentName = null;
+        mCurrentText = null;
+
+        mAttributeCount = 0;
+        mAttributes = new Attribute[8];
+        for (int i = 0; i < mAttributes.length; i++) {
+            mAttributes[i] = new Attribute();
+        }
+
+        try {
+            final byte[] magic = new byte[4];
+            mIn.readFully(magic);
+            if (!Arrays.equals(magic, PROTOCOL_MAGIC_VERSION_0)) {
+                throw new IOException("Unexpected magic " + bytesToHexString(magic));
+            }
+
+            // We're willing to immediately consume a START_DOCUMENT if present,
+            // but we're okay if it's missing
+            if (peekNextExternalToken() == START_DOCUMENT) {
+                consumeToken();
+            }
+        } catch (IOException e) {
+            throw new XmlPullParserException(e.toString());
+        }
+    }
+
+    @Override
+    public void setInput(Reader in) throws XmlPullParserException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int next() throws XmlPullParserException, IOException {
+        while (true) {
+            final int token = nextToken();
+            switch (token) {
+                case START_TAG:
+                case END_TAG:
+                case END_DOCUMENT:
+                    return token;
+                case TEXT:
+                    consumeAdditionalText();
+                    // Per interface docs, empty text regions are skipped
+                    if (mCurrentText == null || mCurrentText.length() == 0) {
+                        continue;
+                    } else {
+                        return TEXT;
+                    }
+            }
+        }
+    }
+
+    @Override
+    public int nextToken() throws XmlPullParserException, IOException {
+        if (mCurrentToken == XmlPullParser.END_TAG) {
+            mCurrentDepth--;
+        }
+
+        int token;
+        try {
+            token = peekNextExternalToken();
+            consumeToken();
+        } catch (EOFException e) {
+            token = END_DOCUMENT;
+        }
+        switch (token) {
+            case XmlPullParser.START_TAG:
+                // We need to peek forward to find the next external token so
+                // that we parse all pending INTERNAL_ATTRIBUTE tokens
+                peekNextExternalToken();
+                mCurrentDepth++;
+                break;
+        }
+        mCurrentToken = token;
+        return token;
+    }
+
+    /**
+     * Peek at the next "external" token without consuming it.
+     * <p>
+     * External tokens, such as {@link #START_TAG}, are expected by typical
+     * {@link XmlPullParser} clients. In contrast, internal tokens, such as
+     * {@link #ATTRIBUTE}, are not expected by typical clients.
+     * <p>
+     * This method consumes any internal events until it reaches the next
+     * external event.
+     */
+    private int peekNextExternalToken() throws IOException, XmlPullParserException {
+        while (true) {
+            final int token = peekNextToken();
+            switch (token) {
+                case ATTRIBUTE:
+                    consumeToken();
+                    continue;
+                default:
+                    return token;
+            }
+        }
+    }
+
+    /**
+     * Peek at the next token in the underlying stream without consuming it.
+     */
+    private int peekNextToken() throws IOException {
+        return mIn.peekByte() & 0x0f;
+    }
+
+    /**
+     * Parse and consume the next token in the underlying stream.
+     */
+    private void consumeToken() throws IOException, XmlPullParserException {
+        final int event = mIn.readByte();
+        final int token = event & 0x0f;
+        final int type = event & 0xf0;
+        switch (token) {
+            case ATTRIBUTE: {
+                final Attribute attr = obtainAttribute();
+                attr.name = mIn.readInternedUTF();
+                attr.type = type;
+                switch (type) {
+                    case TYPE_NULL:
+                    case TYPE_BOOLEAN_TRUE:
+                    case TYPE_BOOLEAN_FALSE:
+                        // Nothing extra to fill in
+                        break;
+                    case TYPE_STRING:
+                        attr.valueString = mIn.readUTF();
+                        break;
+                    case TYPE_STRING_INTERNED:
+                        attr.valueString = mIn.readInternedUTF();
+                        break;
+                    case TYPE_BYTES_HEX:
+                    case TYPE_BYTES_BASE64:
+                        final int len = mIn.readUnsignedShort();
+                        final byte[] res = new byte[len];
+                        mIn.readFully(res);
+                        attr.valueBytes = res;
+                        break;
+                    case TYPE_INT:
+                    case TYPE_INT_HEX:
+                        attr.valueInt = mIn.readInt();
+                        break;
+                    case TYPE_LONG:
+                    case TYPE_LONG_HEX:
+                        attr.valueLong = mIn.readLong();
+                        break;
+                    case TYPE_FLOAT:
+                        attr.valueFloat = mIn.readFloat();
+                        break;
+                    case TYPE_DOUBLE:
+                        attr.valueDouble = mIn.readDouble();
+                        break;
+                    default:
+                        throw new IOException("Unexpected data type " + type);
+                }
+                break;
+            }
+            case XmlPullParser.START_DOCUMENT: {
+                break;
+            }
+            case XmlPullParser.END_DOCUMENT: {
+                break;
+            }
+            case XmlPullParser.START_TAG: {
+                mCurrentName = mIn.readInternedUTF();
+                resetAttributes();
+                break;
+            }
+            case XmlPullParser.END_TAG: {
+                mCurrentName = mIn.readInternedUTF();
+                resetAttributes();
+                break;
+            }
+            case XmlPullParser.TEXT:
+            case XmlPullParser.CDSECT:
+            case XmlPullParser.PROCESSING_INSTRUCTION:
+            case XmlPullParser.COMMENT:
+            case XmlPullParser.DOCDECL:
+            case XmlPullParser.IGNORABLE_WHITESPACE: {
+                mCurrentText = mIn.readUTF();
+                break;
+            }
+            case XmlPullParser.ENTITY_REF: {
+                mCurrentName = mIn.readUTF();
+                mCurrentText = resolveEntity(mCurrentName);
+                break;
+            }
+            default: {
+                throw new IOException("Unknown token " + token + " with type " + type);
+            }
+        }
+    }
+
+    /**
+     * When the current tag is {@link #TEXT}, consume all subsequent "text"
+     * events, as described by {@link #next}. When finished, the current event
+     * will still be {@link #TEXT}.
+     */
+    private void consumeAdditionalText() throws IOException, XmlPullParserException {
+        String combinedText = mCurrentText;
+        while (true) {
+            final int token = peekNextExternalToken();
+            switch (token) {
+                case COMMENT:
+                case PROCESSING_INSTRUCTION:
+                    // Quietly consumed
+                    consumeToken();
+                    break;
+                case TEXT:
+                case CDSECT:
+                case ENTITY_REF:
+                    // Additional text regions collected
+                    consumeToken();
+                    combinedText += mCurrentText;
+                    break;
+                default:
+                    // Next token is something non-text, so wrap things up
+                    mCurrentToken = TEXT;
+                    mCurrentName = null;
+                    mCurrentText = combinedText;
+                    return;
+            }
+        }
+    }
+
+    static @NonNull String resolveEntity(@NonNull String entity)
+            throws XmlPullParserException {
+        switch (entity) {
+            case "lt": return "<";
+            case "gt": return ">";
+            case "amp": return "&";
+            case "apos": return "'";
+            case "quot": return "\"";
+        }
+        if (entity.length() > 1 && entity.charAt(0) == '#') {
+            final char c = (char) Integer.parseInt(entity.substring(1));
+            return new String(new char[] { c });
+        }
+        throw new XmlPullParserException("Unknown entity " + entity);
+    }
+
+    @Override
+    public void require(int type, String namespace, String name)
+            throws XmlPullParserException, IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        if (mCurrentToken != type || !Objects.equals(mCurrentName, name)) {
+            throw new XmlPullParserException(getPositionDescription());
+        }
+    }
+
+    @Override
+    public String nextText() throws XmlPullParserException, IOException {
+        if (getEventType() != START_TAG) {
+            throw new XmlPullParserException(getPositionDescription());
+        }
+        int eventType = next();
+        if (eventType == TEXT) {
+            String result = getText();
+            eventType = next();
+            if (eventType != END_TAG) {
+                throw new XmlPullParserException(getPositionDescription());
+            }
+            return result;
+        } else if (eventType == END_TAG) {
+            return "";
+        } else {
+            throw new XmlPullParserException(getPositionDescription());
+        }
+    }
+
+    @Override
+    public int nextTag() throws XmlPullParserException, IOException {
+        int eventType = next();
+        if (eventType == TEXT && isWhitespace()) {
+            eventType = next();
+        }
+        if (eventType != START_TAG && eventType != END_TAG) {
+            throw new XmlPullParserException(getPositionDescription());
+        }
+        return eventType;
+    }
+
+    /**
+     * Allocate and return a new {@link Attribute} associated with the tag being
+     * currently processed. This will automatically grow the internal pool as
+     * needed.
+     */
+    private @NonNull Attribute obtainAttribute() {
+        if (mAttributeCount == mAttributes.length) {
+            final int before = mAttributes.length;
+            final int after = before + (before >> 1);
+            mAttributes = Arrays.copyOf(mAttributes, after);
+            for (int i = before; i < after; i++) {
+                mAttributes[i] = new Attribute();
+            }
+        }
+        return mAttributes[mAttributeCount++];
+    }
+
+    /**
+     * Clear any {@link Attribute} instances that have been allocated by
+     * {@link #obtainAttribute()}, returning them into the pool for recycling.
+     */
+    private void resetAttributes() {
+        for (int i = 0; i < mAttributeCount; i++) {
+            mAttributes[i].reset();
+        }
+        mAttributeCount = 0;
+    }
+
+    /**
+     * Search through the pool of currently allocated {@link Attribute}
+     * instances for one that matches the given name.
+     */
+    private @NonNull Attribute findAttribute(@NonNull String name) throws IOException {
+        for (int i = 0; i < mAttributeCount; i++) {
+            if (Objects.equals(mAttributes[i].name, name)) {
+                return mAttributes[i];
+            }
+        }
+        throw new IOException("Missing attribute " + name);
+    }
+
+    @Override
+    public String getAttributeValue(String namespace, String name) {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        try {
+            return findAttribute(name).getValueString();
+        } catch (IOException e) {
+            // Missing attributes default to null
+            return null;
+        }
+    }
+
+    @Override
+    public String getAttributeValue(int index) {
+        return mAttributes[index].getValueString();
+    }
+
+    @Override
+    public byte[] getAttributeBytesHex(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        return findAttribute(name).getValueBytesHex();
+    }
+
+    @Override
+    public byte[] getAttributeBytesBase64(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        return findAttribute(name).getValueBytesBase64();
+    }
+
+    @Override
+    public int getAttributeInt(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        return findAttribute(name).getValueInt();
+    }
+
+    @Override
+    public int getAttributeIntHex(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        return findAttribute(name).getValueIntHex();
+    }
+
+    @Override
+    public long getAttributeLong(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        return findAttribute(name).getValueLong();
+    }
+
+    @Override
+    public long getAttributeLongHex(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        return findAttribute(name).getValueLongHex();
+    }
+
+    @Override
+    public float getAttributeFloat(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        return findAttribute(name).getValueFloat();
+    }
+
+    @Override
+    public double getAttributeDouble(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        return findAttribute(name).getValueDouble();
+    }
+
+    @Override
+    public boolean getAttributeBoolean(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        return findAttribute(name).getValueBoolean();
+    }
+
+    @Override
+    public String getText() {
+        return mCurrentText;
+    }
+
+    @Override
+    public char[] getTextCharacters(int[] holderForStartAndLength) {
+        final char[] chars = mCurrentText.toCharArray();
+        holderForStartAndLength[0] = 0;
+        holderForStartAndLength[1] = chars.length;
+        return chars;
+    }
+
+    @Override
+    public String getInputEncoding() {
+        return StandardCharsets.UTF_8.name();
+    }
+
+    @Override
+    public int getDepth() {
+        return mCurrentDepth;
+    }
+
+    @Override
+    public String getPositionDescription() {
+        // Not very helpful, but it's the best information we have
+        return "Token " + mCurrentToken + " at depth " + mCurrentDepth;
+    }
+
+    @Override
+    public int getLineNumber() {
+        return -1;
+    }
+
+    @Override
+    public int getColumnNumber() {
+        return -1;
+    }
+
+    @Override
+    public boolean isWhitespace() throws XmlPullParserException {
+        switch (mCurrentToken) {
+            case IGNORABLE_WHITESPACE:
+                return true;
+            case TEXT:
+            case CDSECT:
+                return !TextUtils.isGraphic(mCurrentText);
+            default:
+                throw new XmlPullParserException("Not applicable for token " + mCurrentToken);
+        }
+    }
+
+    @Override
+    public String getNamespace() {
+        switch (mCurrentToken) {
+            case START_TAG:
+            case END_TAG:
+                // Namespaces are unsupported
+                return NO_NAMESPACE;
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public String getName() {
+        return mCurrentName;
+    }
+
+    @Override
+    public String getPrefix() {
+        // Prefixes are not supported
+        return null;
+    }
+
+    @Override
+    public boolean isEmptyElementTag() throws XmlPullParserException {
+        switch (mCurrentToken) {
+            case START_TAG:
+                try {
+                    return (peekNextExternalToken() == END_TAG);
+                } catch (IOException e) {
+                    throw new XmlPullParserException(e.toString());
+                }
+            default:
+                throw new XmlPullParserException("Not at START_TAG");
+        }
+    }
+
+    @Override
+    public int getAttributeCount() {
+        return mAttributeCount;
+    }
+
+    @Override
+    public String getAttributeNamespace(int index) {
+        // Namespaces are unsupported
+        return NO_NAMESPACE;
+    }
+
+    @Override
+    public String getAttributeName(int index) {
+        return mAttributes[index].name;
+    }
+
+    @Override
+    public String getAttributePrefix(int index) {
+        // Prefixes are not supported
+        return null;
+    }
+
+    @Override
+    public String getAttributeType(int index) {
+        // Validation is not supported
+        return "CDATA";
+    }
+
+    @Override
+    public boolean isAttributeDefault(int index) {
+        // Validation is not supported
+        return false;
+    }
+
+    @Override
+    public int getEventType() throws XmlPullParserException {
+        return mCurrentToken;
+    }
+
+    @Override
+    public int getNamespaceCount(int depth) throws XmlPullParserException {
+        // Namespaces are unsupported
+        return 0;
+    }
+
+    @Override
+    public String getNamespacePrefix(int pos) throws XmlPullParserException {
+        // Namespaces are unsupported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getNamespaceUri(int pos) throws XmlPullParserException {
+        // Namespaces are unsupported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getNamespace(String prefix) {
+        // Namespaces are unsupported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void defineEntityReplacementText(String entityName, String replacementText)
+            throws XmlPullParserException {
+        // Custom entities are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setFeature(String name, boolean state) throws XmlPullParserException {
+        // Features are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getFeature(String name) {
+        // Features are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setProperty(String name, Object value) throws XmlPullParserException {
+        // Properties are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object getProperty(String name) {
+        // Properties are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    private static IllegalArgumentException illegalNamespace() {
+        throw new IllegalArgumentException("Namespaces are not supported");
+    }
+
+    /**
+     * Holder representing a single attribute. This design enables object
+     * recycling without resorting to autoboxing.
+     * <p>
+     * To support conversion between human-readable XML and binary XML, the
+     * various accessor methods will transparently convert from/to
+     * human-readable values when needed.
+     */
+    private static class Attribute {
+        public String name;
+        public int type;
+
+        public String valueString;
+        public byte[] valueBytes;
+        public int valueInt;
+        public long valueLong;
+        public float valueFloat;
+        public double valueDouble;
+
+        public void reset() {
+            name = null;
+            valueString = null;
+            valueBytes = null;
+        }
+
+        public @Nullable String getValueString() {
+            switch (type) {
+                case TYPE_NULL:
+                    return null;
+                case TYPE_STRING:
+                case TYPE_STRING_INTERNED:
+                    return valueString;
+                case TYPE_BYTES_HEX:
+                    return bytesToHexString(valueBytes);
+                case TYPE_BYTES_BASE64:
+                    return Base64.encodeToString(valueBytes, Base64.NO_WRAP);
+                case TYPE_INT:
+                    return Integer.toString(valueInt);
+                case TYPE_INT_HEX:
+                    return Integer.toString(valueInt, 16);
+                case TYPE_LONG:
+                    return Long.toString(valueLong);
+                case TYPE_LONG_HEX:
+                    return Long.toString(valueLong, 16);
+                case TYPE_FLOAT:
+                    return Float.toString(valueFloat);
+                case TYPE_DOUBLE:
+                    return Double.toString(valueDouble);
+                case TYPE_BOOLEAN_TRUE:
+                    return "true";
+                case TYPE_BOOLEAN_FALSE:
+                    return "false";
+                default:
+                    // Unknown data type; null is the best we can offer
+                    return null;
+            }
+        }
+
+        public @Nullable byte[] getValueBytesHex() throws IOException {
+            switch (type) {
+                case TYPE_NULL:
+                    return null;
+                case TYPE_BYTES_HEX:
+                case TYPE_BYTES_BASE64:
+                    return valueBytes;
+                case TYPE_STRING:
+                    return hexStringToBytes(valueString);
+                default:
+                    throw new IOException("Invalid conversion from " + type);
+            }
+        }
+
+        public @Nullable byte[] getValueBytesBase64() throws IOException {
+            switch (type) {
+                case TYPE_NULL:
+                    return null;
+                case TYPE_BYTES_HEX:
+                case TYPE_BYTES_BASE64:
+                    return valueBytes;
+                case TYPE_STRING:
+                    return Base64.decode(valueString, Base64.NO_WRAP);
+                default:
+                    throw new IOException("Invalid conversion from " + type);
+            }
+        }
+
+        public int getValueInt() throws IOException {
+            switch (type) {
+                case TYPE_INT:
+                case TYPE_INT_HEX:
+                    return valueInt;
+                case TYPE_STRING:
+                    return Integer.parseInt(valueString);
+                default:
+                    throw new IOException("Invalid conversion from " + type);
+            }
+        }
+
+        public int getValueIntHex() throws IOException {
+            switch (type) {
+                case TYPE_INT:
+                case TYPE_INT_HEX:
+                    return valueInt;
+                case TYPE_STRING:
+                    return Integer.parseInt(valueString, 16);
+                default:
+                    throw new IOException("Invalid conversion from " + type);
+            }
+        }
+
+        public long getValueLong() throws IOException {
+            switch (type) {
+                case TYPE_LONG:
+                case TYPE_LONG_HEX:
+                    return valueLong;
+                case TYPE_STRING:
+                    return Long.parseLong(valueString);
+                default:
+                    throw new IOException("Invalid conversion from " + type);
+            }
+        }
+
+        public long getValueLongHex() throws IOException {
+            switch (type) {
+                case TYPE_LONG:
+                case TYPE_LONG_HEX:
+                    return valueLong;
+                case TYPE_STRING:
+                    return Long.parseLong(valueString, 16);
+                default:
+                    throw new IOException("Invalid conversion from " + type);
+            }
+        }
+
+        public float getValueFloat() throws IOException {
+            switch (type) {
+                case TYPE_FLOAT:
+                    return valueFloat;
+                case TYPE_STRING:
+                    return Float.parseFloat(valueString);
+                default:
+                    throw new IOException("Invalid conversion from " + type);
+            }
+        }
+
+        public double getValueDouble() throws IOException {
+            switch (type) {
+                case TYPE_DOUBLE:
+                    return valueDouble;
+                case TYPE_STRING:
+                    return Double.parseDouble(valueString);
+                default:
+                    throw new IOException("Invalid conversion from " + type);
+            }
+        }
+
+        public boolean getValueBoolean() throws IOException {
+            switch (type) {
+                case TYPE_BOOLEAN_TRUE:
+                    return true;
+                case TYPE_BOOLEAN_FALSE:
+                    return false;
+                case TYPE_STRING:
+                    if ("true".equalsIgnoreCase(valueString)) {
+                        return true;
+                    } else if ("false".equalsIgnoreCase(valueString)) {
+                        return false;
+                    } else {
+                        throw new IOException("Invalid boolean: " + valueString);
+                    }
+                default:
+                    throw new IOException("Invalid conversion from " + type);
+            }
+        }
+    }
+
+    // NOTE: To support unbundled clients, we include an inlined copy
+    // of hex conversion logic from HexDump below
+    private final static char[] HEX_DIGITS =
+            { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+    private static int toByte(char c) throws IOException {
+        if (c >= '0' && c <= '9') return (c - '0');
+        if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
+        if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
+        throw new IOException("Invalid hex char '" + c + "'");
+    }
+
+    static String bytesToHexString(byte[] value) {
+        final int length = value.length;
+        final char[] buf = new char[length * 2];
+        int bufIndex = 0;
+        for (int i = 0; i < length; i++) {
+            byte b = value[i];
+            buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
+            buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
+        }
+        return new String(buf);
+    }
+
+    static byte[] hexStringToBytes(String value) throws IOException {
+        final int length = value.length();
+        if (length % 2 != 0) {
+            throw new IOException("Invalid hex length " + length);
+        }
+        byte[] buffer = new byte[length / 2];
+        for (int i = 0; i < length; i += 2) {
+            buffer[i / 2] = (byte) ((toByte(value.charAt(i)) << 4)
+                    | toByte(value.charAt(i + 1)));
+        }
+        return buffer;
+    }
+}
diff --git a/core/java/com/android/internal/util/BinaryXmlSerializer.java b/core/java/com/android/internal/util/BinaryXmlSerializer.java
new file mode 100644
index 0000000..d3fcf71
--- /dev/null
+++ b/core/java/com/android/internal/util/BinaryXmlSerializer.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2020 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.internal.util;
+
+import static org.xmlpull.v1.XmlPullParser.CDSECT;
+import static org.xmlpull.v1.XmlPullParser.COMMENT;
+import static org.xmlpull.v1.XmlPullParser.DOCDECL;
+import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.END_TAG;
+import static org.xmlpull.v1.XmlPullParser.ENTITY_REF;
+import static org.xmlpull.v1.XmlPullParser.IGNORABLE_WHITESPACE;
+import static org.xmlpull.v1.XmlPullParser.PROCESSING_INSTRUCTION;
+import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.START_TAG;
+import static org.xmlpull.v1.XmlPullParser.TEXT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * Serializer that writes XML documents using a custom binary wire protocol
+ * which benchmarking has shown to be 4.3x faster and use 2.4x less disk space
+ * than {@code Xml.newFastSerializer()} for a typical {@code packages.xml}.
+ * <p>
+ * The high-level design of the wire protocol is to directly serialize the event
+ * stream, while efficiently and compactly writing strongly-typed primitives
+ * delivered through the {@link TypedXmlSerializer} interface.
+ * <p>
+ * Each serialized event is a single byte where the lower half is a normal
+ * {@link XmlPullParser} token and the upper half is an optional data type
+ * signal, such as {@link #TYPE_INT}.
+ * <p>
+ * This serializer has some specific limitations:
+ * <ul>
+ * <li>Only the UTF-8 encoding is supported.
+ * <li>Variable length values, such as {@code byte[]} or {@link String}, are
+ * limited to 65,535 bytes in length. Note that {@link String} values are stored
+ * as UTF-8 on the wire.
+ * <li>Namespaces, prefixes, properties, and options are unsupported.
+ * </ul>
+ */
+public final class BinaryXmlSerializer implements TypedXmlSerializer {
+    /**
+     * The wire protocol always begins with a well-known magic value of
+     * {@code ABX_}, representing "Android Binary XML." The final byte is a
+     * version number which may be incremented as the protocol changes.
+     */
+    static final byte[] PROTOCOL_MAGIC_VERSION_0 = new byte[] { 0x41, 0x42, 0x58, 0x00 };
+
+    /**
+     * Internal token which represents an attribute associated with the most
+     * recent {@link #START_TAG} token.
+     */
+    static final int ATTRIBUTE = 15;
+
+    static final int TYPE_NULL = 1 << 4;
+    static final int TYPE_STRING = 2 << 4;
+    static final int TYPE_STRING_INTERNED = 3 << 4;
+    static final int TYPE_BYTES_HEX = 4 << 4;
+    static final int TYPE_BYTES_BASE64 = 5 << 4;
+    static final int TYPE_INT = 6 << 4;
+    static final int TYPE_INT_HEX = 7 << 4;
+    static final int TYPE_LONG = 8 << 4;
+    static final int TYPE_LONG_HEX = 9 << 4;
+    static final int TYPE_FLOAT = 10 << 4;
+    static final int TYPE_DOUBLE = 11 << 4;
+    static final int TYPE_BOOLEAN_TRUE = 12 << 4;
+    static final int TYPE_BOOLEAN_FALSE = 13 << 4;
+
+    /**
+     * Default buffer size, which matches {@code FastXmlSerializer}. This should
+     * be kept in sync with {@link BinaryXmlPullParser}.
+     */
+    private static final int BUFFER_SIZE = 32_768;
+
+    private FastDataOutput mOut;
+
+    /**
+     * Stack of tags which are currently active via {@link #startTag} and which
+     * haven't been terminated via {@link #endTag}.
+     */
+    private int mTagCount = 0;
+    private String[] mTagNames;
+
+    /**
+     * Write the given token and optional {@link String} into our buffer.
+     */
+    private void writeToken(int token, @Nullable String text) throws IOException {
+        if (text != null) {
+            mOut.writeByte(token | TYPE_STRING);
+            mOut.writeUTF(text);
+        } else {
+            mOut.writeByte(token | TYPE_NULL);
+        }
+    }
+
+    @Override
+    public void setOutput(@NonNull OutputStream os, @Nullable String encoding) throws IOException {
+        if (encoding != null && !StandardCharsets.UTF_8.name().equals(encoding)) {
+            throw new UnsupportedOperationException();
+        }
+
+        mOut = new FastDataOutput(os, BUFFER_SIZE);
+        mOut.write(PROTOCOL_MAGIC_VERSION_0);
+
+        mTagCount = 0;
+        mTagNames = new String[8];
+    }
+
+    @Override
+    public void setOutput(Writer writer) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void flush() throws IOException {
+        mOut.flush();
+    }
+
+    @Override
+    public void startDocument(@Nullable String encoding, @Nullable Boolean standalone)
+            throws IOException {
+        if (encoding != null && !StandardCharsets.UTF_8.name().equals(encoding)) {
+            throw new UnsupportedOperationException();
+        }
+        mOut.writeByte(START_DOCUMENT | TYPE_NULL);
+    }
+
+    @Override
+    public void endDocument() throws IOException {
+        mOut.writeByte(END_DOCUMENT | TYPE_NULL);
+        flush();
+    }
+
+    @Override
+    public int getDepth() {
+        return mTagCount;
+    }
+
+    @Override
+    public String getNamespace() {
+        // Namespaces are unsupported
+        return XmlPullParser.NO_NAMESPACE;
+    }
+
+    @Override
+    public String getName() {
+        return mTagNames[mTagCount - 1];
+    }
+
+    @Override
+    public XmlSerializer startTag(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        if (mTagCount == mTagNames.length) {
+            mTagNames = Arrays.copyOf(mTagNames, mTagCount + (mTagCount >> 1));
+        }
+        mTagNames[mTagCount++] = name;
+        mOut.writeByte(START_TAG | TYPE_STRING_INTERNED);
+        mOut.writeInternedUTF(name);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer endTag(String namespace, String name) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mTagCount--;
+        mOut.writeByte(END_TAG | TYPE_STRING_INTERNED);
+        mOut.writeInternedUTF(name);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attribute(String namespace, String name, String value) throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mOut.writeByte(ATTRIBUTE | TYPE_STRING);
+        mOut.writeInternedUTF(name);
+        mOut.writeUTF(value);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attributeInterned(String namespace, String name, String value)
+            throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mOut.writeByte(ATTRIBUTE | TYPE_STRING_INTERNED);
+        mOut.writeInternedUTF(name);
+        mOut.writeInternedUTF(value);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attributeBytesHex(String namespace, String name, byte[] value)
+            throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mOut.writeByte(ATTRIBUTE | TYPE_BYTES_HEX);
+        mOut.writeInternedUTF(name);
+        mOut.writeShort(value.length);
+        mOut.write(value);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
+            throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
+        mOut.writeInternedUTF(name);
+        mOut.writeShort(value.length);
+        mOut.write(value);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attributeInt(String namespace, String name, int value)
+            throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mOut.writeByte(ATTRIBUTE | TYPE_INT);
+        mOut.writeInternedUTF(name);
+        mOut.writeInt(value);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attributeIntHex(String namespace, String name, int value)
+            throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mOut.writeByte(ATTRIBUTE | TYPE_INT_HEX);
+        mOut.writeInternedUTF(name);
+        mOut.writeInt(value);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attributeLong(String namespace, String name, long value)
+            throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mOut.writeByte(ATTRIBUTE | TYPE_LONG);
+        mOut.writeInternedUTF(name);
+        mOut.writeLong(value);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attributeLongHex(String namespace, String name, long value)
+            throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mOut.writeByte(ATTRIBUTE | TYPE_LONG_HEX);
+        mOut.writeInternedUTF(name);
+        mOut.writeLong(value);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attributeFloat(String namespace, String name, float value)
+            throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mOut.writeByte(ATTRIBUTE | TYPE_FLOAT);
+        mOut.writeInternedUTF(name);
+        mOut.writeFloat(value);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attributeDouble(String namespace, String name, double value)
+            throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        mOut.writeByte(ATTRIBUTE | TYPE_DOUBLE);
+        mOut.writeInternedUTF(name);
+        mOut.writeDouble(value);
+        return this;
+    }
+
+    @Override
+    public XmlSerializer attributeBoolean(String namespace, String name, boolean value)
+            throws IOException {
+        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
+        if (value) {
+            mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_TRUE);
+            mOut.writeInternedUTF(name);
+        } else {
+            mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_FALSE);
+            mOut.writeInternedUTF(name);
+        }
+        return this;
+    }
+
+    @Override
+    public XmlSerializer text(char[] buf, int start, int len) throws IOException {
+        writeToken(TEXT, new String(buf, start, len));
+        return this;
+    }
+
+    @Override
+    public XmlSerializer text(String text) throws IOException {
+        writeToken(TEXT, text);
+        return this;
+    }
+
+    @Override
+    public void cdsect(String text) throws IOException {
+        writeToken(CDSECT, text);
+    }
+
+    @Override
+    public void entityRef(String text) throws IOException {
+        writeToken(ENTITY_REF, text);
+    }
+
+    @Override
+    public void processingInstruction(String text) throws IOException {
+        writeToken(PROCESSING_INSTRUCTION, text);
+    }
+
+    @Override
+    public void comment(String text) throws IOException {
+        writeToken(COMMENT, text);
+    }
+
+    @Override
+    public void docdecl(String text) throws IOException {
+        writeToken(DOCDECL, text);
+    }
+
+    @Override
+    public void ignorableWhitespace(String text) throws IOException {
+        writeToken(IGNORABLE_WHITESPACE, text);
+    }
+
+    @Override
+    public void setFeature(String name, boolean state) {
+        // Quietly handle no-op features
+        if ("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)) {
+            return;
+        }
+        // Features are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getFeature(String name) {
+        // Features are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setProperty(String name, Object value) {
+        // Properties are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object getProperty(String name) {
+        // Properties are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setPrefix(String prefix, String namespace) {
+        // Prefixes are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getPrefix(String namespace, boolean generatePrefix) {
+        // Prefixes are not supported
+        throw new UnsupportedOperationException();
+    }
+
+    private static IllegalArgumentException illegalNamespace() {
+        throw new IllegalArgumentException("Namespaces are not supported");
+    }
+}
diff --git a/core/tests/coretests/src/android/util/BinaryXmlTest.java b/core/tests/coretests/src/android/util/BinaryXmlTest.java
new file mode 100644
index 0000000..be63a0e
--- /dev/null
+++ b/core/tests/coretests/src/android/util/BinaryXmlTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 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 android.util.XmlTest.assertNext;
+import static android.util.XmlTest.buildPersistableBundle;
+import static android.util.XmlTest.doPersistableBundleRead;
+import static android.util.XmlTest.doPersistableBundleWrite;
+
+import static org.junit.Assert.assertEquals;
+import static org.xmlpull.v1.XmlPullParser.START_TAG;
+
+import android.os.PersistableBundle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+
+@RunWith(AndroidJUnit4.class)
+public class BinaryXmlTest {
+    /**
+     * Verify that we can write and read large numbers of interned
+     * {@link String} values.
+     */
+    @Test
+    public void testLargeInterned_Binary() throws Exception {
+        // We're okay with the tag itself being interned
+        final int count = (1 << 16) - 2;
+
+        final TypedXmlSerializer out = Xml.newBinarySerializer();
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        out.setOutput(os, StandardCharsets.UTF_8.name());
+        out.startTag(null, "tag");
+        for (int i = 0; i < count; i++) {
+            out.attribute(null, "name" + i, "value");
+        }
+        out.endTag(null, "tag");
+        out.flush();
+
+        final TypedXmlPullParser in = Xml.newBinaryPullParser();
+        final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
+        in.setInput(is, StandardCharsets.UTF_8.name());
+        assertNext(in, START_TAG, "tag", 1);
+        assertEquals(count, in.getAttributeCount());
+    }
+
+    @Test
+    public void testTranscode_FastToBinary() throws Exception {
+        doTranscode(Xml.newFastSerializer(), Xml.newFastPullParser(),
+                Xml.newBinarySerializer(), Xml.newBinaryPullParser());
+    }
+
+    @Test
+    public void testTranscode_BinaryToFast() throws Exception {
+        doTranscode(Xml.newBinarySerializer(), Xml.newBinaryPullParser(),
+                Xml.newFastSerializer(), Xml.newFastPullParser());
+    }
+
+    /**
+     * Verify that a complex {@link PersistableBundle} can be transcoded using
+     * the two given formats with the original structure intact.
+     */
+    private static void doTranscode(TypedXmlSerializer firstOut, TypedXmlPullParser firstIn,
+            TypedXmlSerializer secondOut, TypedXmlPullParser secondIn) throws Exception {
+        final PersistableBundle expected = buildPersistableBundle();
+        final byte[] firstRaw = doPersistableBundleWrite(firstOut, expected);
+
+        // Perform actual transcoding between the two formats
+        final ByteArrayInputStream is = new ByteArrayInputStream(firstRaw);
+        firstIn.setInput(is, StandardCharsets.UTF_8.name());
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        secondOut.setOutput(os, StandardCharsets.UTF_8.name());
+        Xml.copy(firstIn, secondOut);
+
+        // Yes, this string-based check is fragile, but kindofEquals() is broken
+        // when working with nested objects and arrays
+        final PersistableBundle actual = doPersistableBundleRead(secondIn, os.toByteArray());
+        assertEquals(expected.toString(), actual.toString());
+    }
+}
diff --git a/core/tests/coretests/src/android/util/XmlTest.java b/core/tests/coretests/src/android/util/XmlTest.java
index 602b672..2ae9cdf 100644
--- a/core/tests/coretests/src/android/util/XmlTest.java
+++ b/core/tests/coretests/src/android/util/XmlTest.java
@@ -52,13 +52,19 @@
                 Xml.newFastPullParser());
     }
 
+    @Test
+    public void testLargeValues_Binary() throws Exception {
+        doLargeValues(Xml.newBinarySerializer(),
+                Xml.newBinaryPullParser());
+    }
+
     /**
      * Verify that we can write and read large {@link String} and {@code byte[]}
      * without issues.
      */
     private static void doLargeValues(TypedXmlSerializer out, TypedXmlPullParser in)
             throws Exception {
-        final char[] chars = new char[32_768];
+        final char[] chars = new char[(1 << 16) - 1];
         Arrays.fill(chars, '!');
 
         final String string = new String(chars);
@@ -93,6 +99,12 @@
                 Xml.newFastPullParser());
     }
 
+    @Test
+    public void testPersistableBundle_Binary() throws Exception {
+        doPersistableBundle(Xml.newBinarySerializer(),
+                Xml.newBinaryPullParser());
+    }
+
     /**
      * Verify that a complex {@link PersistableBundle} can be serialized out and
      * then parsed in with the original structure intact.
@@ -108,7 +120,7 @@
         assertEquals(expected.toString(), actual.toString());
     }
 
-    private static PersistableBundle buildPersistableBundle() {
+    static PersistableBundle buildPersistableBundle() {
         final PersistableBundle outer = new PersistableBundle();
 
         outer.putBoolean("boolean", true);
@@ -130,7 +142,7 @@
         return outer;
     }
 
-    private static byte[] doPersistableBundleWrite(TypedXmlSerializer out, PersistableBundle bundle)
+    static byte[] doPersistableBundleWrite(TypedXmlSerializer out, PersistableBundle bundle)
             throws Exception {
         // We purposefully omit START/END_DOCUMENT events here to verify correct
         // behavior of what PersistableBundle does internally
@@ -143,7 +155,7 @@
         return os.toByteArray();
     }
 
-    private static PersistableBundle doPersistableBundleRead(TypedXmlPullParser in, byte[] raw)
+    static PersistableBundle doPersistableBundleRead(TypedXmlPullParser in, byte[] raw)
             throws Exception {
         final ByteArrayInputStream is = new ByteArrayInputStream(raw);
         in.setInput(is, StandardCharsets.UTF_8.name());
@@ -163,6 +175,12 @@
                 Xml.newFastPullParser());
     }
 
+    @Test
+    public void testVerify_Binary() throws Exception {
+        doVerify(Xml.newBinarySerializer(),
+                Xml.newBinaryPullParser());
+    }
+
     /**
      * Verify that example test data is correctly serialized and parsed
      * end-to-end using the given objects.
@@ -268,7 +286,7 @@
         assertNext(in, END_DOCUMENT);
     }
 
-    private static void assertNext(TypedXmlPullParser in, int token) throws Exception {
+    static void assertNext(TypedXmlPullParser in, int token) throws Exception {
         // We're willing to skip over empty text regions, which some
         // serializers emit transparently
         int event;
@@ -278,7 +296,7 @@
         assertEquals("getEventType", token, in.getEventType());
     }
 
-    private static void assertNext(TypedXmlPullParser in, int token, String name, int depth)
+    static void assertNext(TypedXmlPullParser in, int token, String name, int depth)
             throws Exception {
         assertNext(in, token);
         assertEquals("getName", name, in.getName());