Progress towards efficient XML serialization.

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

To set the stage for some long-term improvements, this change
introduces new TypedXmlSerializer and TypedXmlPullParser interfaces
which offer efficient access to primitive attributes and text.

This change also updates XmlUtils to redirect primitive operations
through these new interfaces.

Bug: 171832118
Test: atest FrameworksCoreTests:android.util.XmlTest
Change-Id: I5ba3ad87cf79ca10705a96b98855164a27fee021
diff --git a/core/java/android/util/TypedXmlPullParser.java b/core/java/android/util/TypedXmlPullParser.java
new file mode 100644
index 0000000..5ff7e5d
--- /dev/null
+++ b/core/java/android/util/TypedXmlPullParser.java
@@ -0,0 +1,186 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.IOException;
+
+/**
+ * Specialization of {@link XmlPullParser} which adds explicit methods to
+ * support consistent and efficient conversion of primitive data types.
+ *
+ * @hide
+ */
+public interface TypedXmlPullParser extends XmlPullParser {
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}, or
+     *         {@code null} if malformed or undefined
+     */
+    @Nullable byte[] getAttributeBytesHex(@Nullable String namespace, @NonNull String name)
+            throws IOException;
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}, or
+     *         {@code null} if malformed or undefined
+     */
+    @Nullable byte[] getAttributeBytesBase64(@Nullable String namespace, @NonNull String name)
+            throws IOException;
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}
+     * @throws IOException if the value is malformed or undefined
+     */
+    int getAttributeInt(@Nullable String namespace, @NonNull String name)
+            throws IOException;
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}
+     * @throws IOException if the value is malformed or undefined
+     */
+    int getAttributeIntHex(@Nullable String namespace, @NonNull String name)
+            throws IOException;
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}
+     * @throws IOException if the value is malformed or undefined
+     */
+    long getAttributeLong(@Nullable String namespace, @NonNull String name)
+            throws IOException;
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}
+     * @throws IOException if the value is malformed or undefined
+     */
+    long getAttributeLongHex(@Nullable String namespace, @NonNull String name)
+            throws IOException;
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}
+     * @throws IOException if the value is malformed or undefined
+     */
+    float getAttributeFloat(@Nullable String namespace, @NonNull String name)
+            throws IOException;
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}
+     * @throws IOException if the value is malformed or undefined
+     */
+    double getAttributeDouble(@Nullable String namespace, @NonNull String name)
+            throws IOException;
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}
+     * @throws IOException if the value is malformed or undefined
+     */
+    boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name)
+            throws IOException;
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+     *         default value if the value is malformed or undefined
+     */
+    default int getAttributeInt(@Nullable String namespace, @NonNull String name,
+            int defaultValue) {
+        try {
+            return getAttributeInt(namespace, name);
+        } catch (Exception ignored) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+     *         default value if the value is malformed or undefined
+     */
+    default int getAttributeIntHex(@Nullable String namespace, @NonNull String name,
+            int defaultValue) {
+        try {
+            return getAttributeIntHex(namespace, name);
+        } catch (Exception ignored) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+     *         default value if the value is malformed or undefined
+     */
+    default long getAttributeLong(@Nullable String namespace, @NonNull String name,
+            long defaultValue) {
+        try {
+            return getAttributeLong(namespace, name);
+        } catch (Exception ignored) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+     *         default value if the value is malformed or undefined
+     */
+    default long getAttributeLongHex(@Nullable String namespace, @NonNull String name,
+            long defaultValue) {
+        try {
+            return getAttributeLongHex(namespace, name);
+        } catch (Exception ignored) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+     *         default value if the value is malformed or undefined
+     */
+    default float getAttributeFloat(@Nullable String namespace, @NonNull String name,
+            float defaultValue) {
+        try {
+            return getAttributeFloat(namespace, name);
+        } catch (Exception ignored) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+     *         default value if the value is malformed or undefined
+     */
+    default double getAttributeDouble(@Nullable String namespace, @NonNull String name,
+            double defaultValue) {
+        try {
+            return getAttributeDouble(namespace, name);
+        } catch (Exception ignored) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+     *         default value if the value is malformed or undefined
+     */
+    default boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name,
+            boolean defaultValue) {
+        try {
+            return getAttributeBoolean(namespace, name);
+        } catch (Exception ignored) {
+            return defaultValue;
+        }
+    }
+}
diff --git a/core/java/android/util/TypedXmlSerializer.java b/core/java/android/util/TypedXmlSerializer.java
new file mode 100644
index 0000000..fe5e3e6
--- /dev/null
+++ b/core/java/android/util/TypedXmlSerializer.java
@@ -0,0 +1,103 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * Specialization of {@link XmlSerializer} which adds explicit methods to
+ * support consistent and efficient conversion of primitive data types.
+ *
+ * @hide
+ */
+public interface TypedXmlSerializer extends XmlSerializer {
+    /**
+     * Functionally equivalent to {@link #attribute(String, String, String)} but
+     * with the additional signal that the given value is a candidate for being
+     * canonicalized, similar to {@link String#intern()}.
+     */
+    @NonNull XmlSerializer attributeInterned(@Nullable String namespace, @NonNull String name,
+            @Nullable String value) throws IOException;
+
+    /**
+     * Encode the given strongly-typed value and serialize using
+     * {@link #attribute(String, String, String)}.
+     */
+    @NonNull XmlSerializer attributeBytesHex(@Nullable String namespace, @NonNull String name,
+            byte[] value) throws IOException;
+
+    /**
+     * Encode the given strongly-typed value and serialize using
+     * {@link #attribute(String, String, String)}.
+     */
+    @NonNull XmlSerializer attributeBytesBase64(@Nullable String namespace, @NonNull String name,
+            byte[] value) throws IOException;
+
+    /**
+     * Encode the given strongly-typed value and serialize using
+     * {@link #attribute(String, String, String)}.
+     */
+    @NonNull XmlSerializer attributeInt(@Nullable String namespace, @NonNull String name,
+            int value) throws IOException;
+
+    /**
+     * Encode the given strongly-typed value and serialize using
+     * {@link #attribute(String, String, String)}.
+     */
+    @NonNull XmlSerializer attributeIntHex(@Nullable String namespace, @NonNull String name,
+            int value) throws IOException;
+
+    /**
+     * Encode the given strongly-typed value and serialize using
+     * {@link #attribute(String, String, String)}.
+     */
+    @NonNull XmlSerializer attributeLong(@Nullable String namespace, @NonNull String name,
+            long value) throws IOException;
+
+    /**
+     * Encode the given strongly-typed value and serialize using
+     * {@link #attribute(String, String, String)}.
+     */
+    @NonNull XmlSerializer attributeLongHex(@Nullable String namespace, @NonNull String name,
+            long value) throws IOException;
+
+    /**
+     * Encode the given strongly-typed value and serialize using
+     * {@link #attribute(String, String, String)}.
+     */
+    @NonNull XmlSerializer attributeFloat(@Nullable String namespace, @NonNull String name,
+            float value) throws IOException;
+
+    /**
+     * Encode the given strongly-typed value and serialize using
+     * {@link #attribute(String, String, String)}.
+     */
+    @NonNull XmlSerializer attributeDouble(@Nullable String namespace, @NonNull String name,
+            double value) throws IOException;
+
+    /**
+     * Encode the given strongly-typed value and serialize using
+     * {@link #attribute(String, String, String)}.
+     */
+    @NonNull XmlSerializer attributeBoolean(@Nullable String namespace, @NonNull String name,
+            boolean value) throws IOException;
+}
diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java
index e3b8fec..9849c10 100644
--- a/core/java/android/util/Xml.java
+++ b/core/java/android/util/Xml.java
@@ -16,6 +16,11 @@
 
 package android.util;
 
+import android.annotation.NonNull;
+
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+
 import libcore.util.XmlObjectFactory;
 
 import org.xml.sax.ContentHandler;
@@ -26,11 +31,15 @@
 import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlSerializer;
 
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.Reader;
 import java.io.StringReader;
 import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
 
 /**
  * XML utility methods.
@@ -99,6 +108,45 @@
     }
 
     /**
+     * Creates a new {@link TypedXmlPullParser} which is optimized for use
+     * inside the system, typically by supporting only a basic set of features.
+     * <p>
+     * In particular, the returned parser does not support namespaces, prefixes,
+     * properties, or options.
+     *
+     * @hide
+     */
+    public static @NonNull TypedXmlPullParser newFastPullParser() {
+        return XmlUtils.makeTyped(newPullParser());
+    }
+
+    /**
+     * Creates a new {@link XmlPullParser} which is optimized for use inside the
+     * system, typically by supporting only a basic set of features.
+     * <p>
+     * This returned instance may be configured to read using an efficient
+     * binary format instead of a human-readable text format, depending on
+     * device feature flags.
+     * <p>
+     * To ensure that both formats are detected and transparently handled
+     * correctly, you must shift to using both {@link #resolveSerializer} and
+     * {@link #resolvePullParser}.
+     *
+     * @hide
+     */
+    public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
+            throws IOException {
+        // TODO: add support for binary format
+        final TypedXmlPullParser xml = newFastPullParser();
+        try {
+            xml.setInput(in, StandardCharsets.UTF_8.name());
+        } catch (XmlPullParserException e) {
+            throw new IOException(e);
+        }
+        return xml;
+    }
+
+    /**
      * Creates a new xml serializer.
      */
     public static XmlSerializer newSerializer() {
@@ -106,6 +154,41 @@
     }
 
     /**
+     * Creates a new {@link XmlSerializer} which is optimized for use inside the
+     * system, typically by supporting only a basic set of features.
+     * <p>
+     * In particular, the returned parser does not support namespaces, prefixes,
+     * properties, or options.
+     *
+     * @hide
+     */
+    public static @NonNull TypedXmlSerializer newFastSerializer() {
+        return XmlUtils.makeTyped(new FastXmlSerializer());
+    }
+
+    /**
+     * Creates a new {@link XmlSerializer} which is optimized for use inside the
+     * system, typically by supporting only a basic set of features.
+     * <p>
+     * This returned instance may be configured to write using an efficient
+     * binary format instead of a human-readable text format, depending on
+     * device feature flags.
+     * <p>
+     * To ensure that both formats are detected and transparently handled
+     * correctly, you must shift to using both {@link #resolveSerializer} and
+     * {@link #resolvePullParser}.
+     *
+     * @hide
+     */
+    public static @NonNull TypedXmlSerializer resolveSerializer(@NonNull OutputStream out)
+            throws IOException {
+        // TODO: add support for binary format
+        final TypedXmlSerializer xml = newFastSerializer();
+        xml.setOutput(out, StandardCharsets.UTF_8.name());
+        return xml;
+    }
+
+    /**
      * Supported character encodings.
      */
     public enum Encoding {
diff --git a/core/java/com/android/internal/util/XmlPullParserWrapper.java b/core/java/com/android/internal/util/XmlPullParserWrapper.java
new file mode 100644
index 0000000..efa17ef
--- /dev/null
+++ b/core/java/com/android/internal/util/XmlPullParserWrapper.java
@@ -0,0 +1,189 @@
+/*
+ * 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 android.annotation.NonNull;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.Objects;
+
+/**
+ * Wrapper which delegates all calls through to the given {@link XmlPullParser}.
+ */
+public class XmlPullParserWrapper implements XmlPullParser {
+    private final XmlPullParser mWrapped;
+
+    public XmlPullParserWrapper(@NonNull XmlPullParser wrapped) {
+        mWrapped = Objects.requireNonNull(wrapped);
+    }
+
+    public void setFeature(String name, boolean state) throws XmlPullParserException {
+        mWrapped.setFeature(name, state);
+    }
+
+    public boolean getFeature(String name) {
+        return mWrapped.getFeature(name);
+    }
+
+    public void setProperty(String name, Object value) throws XmlPullParserException {
+        mWrapped.setProperty(name, value);
+    }
+
+    public Object getProperty(String name) {
+        return mWrapped.getProperty(name);
+    }
+
+    public void setInput(Reader in) throws XmlPullParserException {
+        mWrapped.setInput(in);
+    }
+
+    public void setInput(InputStream inputStream, String inputEncoding)
+            throws XmlPullParserException {
+        mWrapped.setInput(inputStream, inputEncoding);
+    }
+
+    public String getInputEncoding() {
+        return mWrapped.getInputEncoding();
+    }
+
+    public void defineEntityReplacementText(String entityName, String replacementText)
+            throws XmlPullParserException {
+        mWrapped.defineEntityReplacementText(entityName, replacementText);
+    }
+
+    public int getNamespaceCount(int depth) throws XmlPullParserException {
+        return mWrapped.getNamespaceCount(depth);
+    }
+
+    public String getNamespacePrefix(int pos) throws XmlPullParserException {
+        return mWrapped.getNamespacePrefix(pos);
+    }
+
+    public String getNamespaceUri(int pos) throws XmlPullParserException {
+        return mWrapped.getNamespaceUri(pos);
+    }
+
+    public String getNamespace(String prefix) {
+        return mWrapped.getNamespace(prefix);
+    }
+
+    public int getDepth() {
+        return mWrapped.getDepth();
+    }
+
+    public String getPositionDescription() {
+        return mWrapped.getPositionDescription();
+    }
+
+    public int getLineNumber() {
+        return mWrapped.getLineNumber();
+    }
+
+    public int getColumnNumber() {
+        return mWrapped.getColumnNumber();
+    }
+
+    public boolean isWhitespace() throws XmlPullParserException {
+        return mWrapped.isWhitespace();
+    }
+
+    public String getText() {
+        return mWrapped.getText();
+    }
+
+    public char[] getTextCharacters(int[] holderForStartAndLength) {
+        return mWrapped.getTextCharacters(holderForStartAndLength);
+    }
+
+    public String getNamespace() {
+        return mWrapped.getNamespace();
+    }
+
+    public String getName() {
+        return mWrapped.getName();
+    }
+
+    public String getPrefix() {
+        return mWrapped.getPrefix();
+    }
+
+    public boolean isEmptyElementTag() throws XmlPullParserException {
+        return mWrapped.isEmptyElementTag();
+    }
+
+    public int getAttributeCount() {
+        return mWrapped.getAttributeCount();
+    }
+
+    public String getAttributeNamespace(int index) {
+        return mWrapped.getAttributeNamespace(index);
+    }
+
+    public String getAttributeName(int index) {
+        return mWrapped.getAttributeName(index);
+    }
+
+    public String getAttributePrefix(int index) {
+        return mWrapped.getAttributePrefix(index);
+    }
+
+    public String getAttributeType(int index) {
+        return mWrapped.getAttributeType(index);
+    }
+
+    public boolean isAttributeDefault(int index) {
+        return mWrapped.isAttributeDefault(index);
+    }
+
+    public String getAttributeValue(int index) {
+        return mWrapped.getAttributeValue(index);
+    }
+
+    public String getAttributeValue(String namespace, String name) {
+        return mWrapped.getAttributeValue(namespace, name);
+    }
+
+    public int getEventType() throws XmlPullParserException {
+        return mWrapped.getEventType();
+    }
+
+    public int next() throws XmlPullParserException, IOException {
+        return mWrapped.next();
+    }
+
+    public int nextToken() throws XmlPullParserException, IOException {
+        return mWrapped.nextToken();
+    }
+
+    public void require(int type, String namespace, String name)
+            throws XmlPullParserException, IOException {
+        mWrapped.require(type, namespace, name);
+    }
+
+    public String nextText() throws XmlPullParserException, IOException {
+        return mWrapped.nextText();
+    }
+
+    public int nextTag() throws XmlPullParserException, IOException {
+        return mWrapped.nextTag();
+    }
+}
diff --git a/core/java/com/android/internal/util/XmlSerializerWrapper.java b/core/java/com/android/internal/util/XmlSerializerWrapper.java
new file mode 100644
index 0000000..2131db0
--- /dev/null
+++ b/core/java/com/android/internal/util/XmlSerializerWrapper.java
@@ -0,0 +1,140 @@
+/*
+ * 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 android.annotation.NonNull;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.util.Objects;
+
+/**
+ * Wrapper which delegates all calls through to the given {@link XmlSerializer}.
+ */
+public class XmlSerializerWrapper {
+    private final XmlSerializer mWrapped;
+
+    public XmlSerializerWrapper(@NonNull XmlSerializer wrapped) {
+        mWrapped = Objects.requireNonNull(wrapped);
+    }
+
+    public void setFeature(String name, boolean state) {
+        mWrapped.setFeature(name, state);
+    }
+
+    public boolean getFeature(String name) {
+        return mWrapped.getFeature(name);
+    }
+
+    public void setProperty(String name, Object value) {
+        mWrapped.setProperty(name, value);
+    }
+
+    public Object getProperty(String name) {
+        return mWrapped.getProperty(name);
+    }
+
+    public void setOutput(OutputStream os, String encoding) throws IOException {
+        mWrapped.setOutput(os, encoding);
+    }
+
+    public void setOutput(Writer writer)
+            throws IOException, IllegalArgumentException, IllegalStateException {
+        mWrapped.setOutput(writer);
+    }
+
+    public void startDocument(String encoding, Boolean standalone) throws IOException {
+        mWrapped.startDocument(encoding, standalone);
+    }
+
+    public void endDocument() throws IOException {
+        mWrapped.endDocument();
+    }
+
+    public void setPrefix(String prefix, String namespace) throws IOException {
+        mWrapped.setPrefix(prefix, namespace);
+    }
+
+    public String getPrefix(String namespace, boolean generatePrefix) {
+        return mWrapped.getPrefix(namespace, generatePrefix);
+    }
+
+    public int getDepth() {
+        return mWrapped.getDepth();
+    }
+
+    public String getNamespace() {
+        return mWrapped.getNamespace();
+    }
+
+    public String getName() {
+        return mWrapped.getName();
+    }
+
+    public XmlSerializer startTag(String namespace, String name) throws IOException {
+        return mWrapped.startTag(namespace, name);
+    }
+
+    public XmlSerializer attribute(String namespace, String name, String value)
+            throws IOException {
+        return mWrapped.attribute(namespace, name, value);
+    }
+
+    public XmlSerializer endTag(String namespace, String name) throws IOException {
+        return mWrapped.endTag(namespace, name);
+    }
+
+    public XmlSerializer text(String text) throws IOException{
+        return mWrapped.text(text);
+    }
+
+    public XmlSerializer text(char[] buf, int start, int len) throws IOException {
+        return mWrapped.text(buf, start, len);
+    }
+
+    public void cdsect(String text)
+            throws IOException, IllegalArgumentException, IllegalStateException {
+        mWrapped.cdsect(text);
+    }
+
+    public void entityRef(String text) throws IOException {
+        mWrapped.entityRef(text);
+    }
+
+    public void processingInstruction(String text) throws IOException {
+        mWrapped.processingInstruction(text);
+    }
+
+    public void comment(String text) throws IOException {
+        mWrapped.comment(text);
+    }
+
+    public void docdecl(String text) throws IOException {
+        mWrapped.docdecl(text);
+    }
+
+    public void ignorableWhitespace(String text) throws IOException {
+        mWrapped.ignorableWhitespace(text);
+    }
+
+    public void flush() throws IOException {
+        mWrapped.flush();
+    }
+}
diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java
index bd6b950..cdd0e04 100644
--- a/core/java/com/android/internal/util/XmlUtils.java
+++ b/core/java/com/android/internal/util/XmlUtils.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.util;
 
+import android.annotation.NonNull;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.CompressFormat;
@@ -24,6 +25,8 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Base64;
+import android.util.TypedXmlPullParser;
+import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import libcore.util.HexEncoding;
@@ -48,9 +51,193 @@
 
 /** {@hide} */
 public class XmlUtils {
-
     private static final String STRING_ARRAY_SEPARATOR = ":";
 
+    private static class ForcedTypedXmlSerializer extends XmlSerializerWrapper
+            implements TypedXmlSerializer {
+        public ForcedTypedXmlSerializer(XmlSerializer wrapped) {
+            super(wrapped);
+        }
+
+        @Override
+        public XmlSerializer attributeInterned(String namespace, String name, String value)
+                throws IOException {
+            return attribute(namespace, name, value);
+        }
+
+        @Override
+        public XmlSerializer attributeBytesHex(String namespace, String name, byte[] value)
+                throws IOException {
+            return attribute(namespace, name, HexDump.toHexString(value));
+        }
+
+        @Override
+        public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
+                throws IOException {
+            return attribute(namespace, name, Base64.encodeToString(value, Base64.NO_WRAP));
+        }
+
+        @Override
+        public XmlSerializer attributeInt(String namespace, String name, int value)
+                throws IOException {
+            return attribute(namespace, name, Integer.toString(value));
+        }
+
+        @Override
+        public XmlSerializer attributeIntHex(String namespace, String name, int value)
+                throws IOException {
+            return attribute(namespace, name, Integer.toString(value, 16));
+        }
+
+        @Override
+        public XmlSerializer attributeLong(String namespace, String name, long value)
+                throws IOException {
+            return attribute(namespace, name, Long.toString(value));
+        }
+
+        @Override
+        public XmlSerializer attributeLongHex(String namespace, String name, long value)
+                throws IOException {
+            return attribute(namespace, name, Long.toString(value, 16));
+        }
+
+        @Override
+        public XmlSerializer attributeFloat(String namespace, String name, float value)
+                throws IOException {
+            return attribute(namespace, name, Float.toString(value));
+        }
+
+        @Override
+        public XmlSerializer attributeDouble(String namespace, String name, double value)
+                throws IOException {
+            return attribute(namespace, name, Double.toString(value));
+        }
+
+        @Override
+        public XmlSerializer attributeBoolean(String namespace, String name, boolean value)
+                throws IOException {
+            return attribute(namespace, name, Boolean.toString(value));
+        }
+    }
+
+    /**
+     * Return a specialization of the given {@link XmlSerializer} which has
+     * explicit methods to support consistent and efficient conversion of
+     * primitive data types.
+     */
+    public static @NonNull TypedXmlSerializer makeTyped(@NonNull XmlSerializer xml) {
+        if (xml instanceof TypedXmlSerializer) {
+            return (TypedXmlSerializer) xml;
+        } else {
+            return new ForcedTypedXmlSerializer(xml);
+        }
+    }
+
+    private static class ForcedTypedXmlPullParser extends XmlPullParserWrapper
+            implements TypedXmlPullParser {
+        public ForcedTypedXmlPullParser(XmlPullParser wrapped) {
+            super(wrapped);
+        }
+
+        @Override
+        public byte[] getAttributeBytesHex(String namespace, String name) throws IOException {
+            try {
+                return HexDump.hexStringToByteArray(getAttributeValue(namespace, name));
+            } catch (Exception e) {
+                throw new IOException("Invalid attribute " + name, e);
+            }
+        }
+
+        @Override
+        public byte[] getAttributeBytesBase64(String namespace, String name) throws IOException {
+            try {
+                return Base64.decode(getAttributeValue(namespace, name), Base64.NO_WRAP);
+            } catch (Exception e) {
+                throw new IOException("Invalid attribute " + name, e);
+            }
+        }
+
+        @Override
+        public int getAttributeInt(String namespace, String name) throws IOException {
+            try {
+                return Integer.parseInt(getAttributeValue(namespace, name));
+            } catch (NumberFormatException e) {
+                throw new IOException("Invalid attribute " + name, e);
+            }
+        }
+
+        @Override
+        public int getAttributeIntHex(String namespace, String name) throws IOException {
+            try {
+                return Integer.parseInt(getAttributeValue(namespace, name), 16);
+            } catch (NumberFormatException e) {
+                throw new IOException("Invalid attribute " + name, e);
+            }
+        }
+
+        @Override
+        public long getAttributeLong(String namespace, String name) throws IOException {
+            try {
+                return Long.parseLong(getAttributeValue(namespace, name));
+            } catch (NumberFormatException e) {
+                throw new IOException("Invalid attribute " + name, e);
+            }
+        }
+
+        @Override
+        public long getAttributeLongHex(String namespace, String name) throws IOException {
+            try {
+                return Long.parseLong(getAttributeValue(namespace, name), 16);
+            } catch (NumberFormatException e) {
+                throw new IOException("Invalid attribute " + name, e);
+            }
+        }
+
+        @Override
+        public float getAttributeFloat(String namespace, String name) throws IOException {
+            try {
+                return Float.parseFloat(getAttributeValue(namespace, name));
+            } catch (NumberFormatException e) {
+                throw new IOException("Invalid attribute " + name, e);
+            }
+        }
+
+        @Override
+        public double getAttributeDouble(String namespace, String name) throws IOException {
+            try {
+                return Double.parseDouble(getAttributeValue(namespace, name));
+            } catch (NumberFormatException e) {
+                throw new IOException("Invalid attribute " + name, e);
+            }
+        }
+
+        @Override
+        public boolean getAttributeBoolean(String namespace, String name) throws IOException {
+            final String value = getAttributeValue(namespace, name);
+            if ("true".equalsIgnoreCase(value)) {
+                return true;
+            } else if ("false".equalsIgnoreCase(value)) {
+                return false;
+            } else {
+                throw new IOException("Invalid attribute " + name,
+                        new IllegalArgumentException("For input string: \"" + value + "\""));
+            }
+        }
+    }
+
+    /**
+     * Return a specialization of the given {@link XmlPullParser} which has
+     * explicit methods to support consistent and efficient conversion of
+     * primitive data types.
+     */
+    public static @NonNull TypedXmlPullParser makeTyped(@NonNull XmlPullParser xml) {
+        if (xml instanceof TypedXmlPullParser) {
+            return (TypedXmlPullParser) xml;
+        } else {
+            return new ForcedTypedXmlPullParser(xml);
+        }
+    }
+
     @UnsupportedAppUsage
     public static void skipCurrentTag(XmlPullParser parser)
             throws XmlPullParserException, IOException {
diff --git a/core/tests/coretests/src/android/util/XmlTest.java b/core/tests/coretests/src/android/util/XmlTest.java
new file mode 100644
index 0000000..602b672
--- /dev/null
+++ b/core/tests/coretests/src/android/util/XmlTest.java
@@ -0,0 +1,287 @@
+/*
+ * 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.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.END_TAG;
+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.os.PersistableBundle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.XmlUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class XmlTest {
+    @Test
+    public void testLargeValues_Normal() throws Exception {
+        doLargeValues(XmlUtils.makeTyped(Xml.newSerializer()),
+                XmlUtils.makeTyped(Xml.newPullParser()));
+    }
+
+    @Test
+    public void testLargeValues_Fast() throws Exception {
+        doLargeValues(Xml.newFastSerializer(),
+                Xml.newFastPullParser());
+    }
+
+    /**
+     * 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];
+        Arrays.fill(chars, '!');
+
+        final String string = new String(chars);
+        final byte[] bytes = string.getBytes();
+        assertEquals(chars.length, bytes.length);
+
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        out.setOutput(os, StandardCharsets.UTF_8.name());
+        out.startTag(null, "tag");
+        out.attribute(null, "string", string);
+        out.attributeBytesBase64(null, "bytes", bytes);
+        out.endTag(null, "tag");
+        out.flush();
+
+        final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
+        in.setInput(is, StandardCharsets.UTF_8.name());
+        assertNext(in, START_TAG, "tag", 1);
+        assertEquals(2, in.getAttributeCount());
+        assertEquals(string, in.getAttributeValue(null, "string"));
+        assertArrayEquals(bytes, in.getAttributeBytesBase64(null, "bytes"));
+    }
+
+    @Test
+    public void testPersistableBundle_Normal() throws Exception {
+        doPersistableBundle(XmlUtils.makeTyped(Xml.newSerializer()),
+                XmlUtils.makeTyped(Xml.newPullParser()));
+    }
+
+    @Test
+    public void testPersistableBundle_Fast() throws Exception {
+        doPersistableBundle(Xml.newFastSerializer(),
+                Xml.newFastPullParser());
+    }
+
+    /**
+     * Verify that a complex {@link PersistableBundle} can be serialized out and
+     * then parsed in with the original structure intact.
+     */
+    private static void doPersistableBundle(TypedXmlSerializer out, TypedXmlPullParser in)
+            throws Exception {
+        final PersistableBundle expected = buildPersistableBundle();
+        final byte[] raw = doPersistableBundleWrite(out, expected);
+
+        // Yes, this string-based check is fragile, but kindofEquals() is broken
+        // when working with nested objects and arrays
+        final PersistableBundle actual = doPersistableBundleRead(in, raw);
+        assertEquals(expected.toString(), actual.toString());
+    }
+
+    private static PersistableBundle buildPersistableBundle() {
+        final PersistableBundle outer = new PersistableBundle();
+
+        outer.putBoolean("boolean", true);
+        outer.putInt("int", 42);
+        outer.putLong("long", 43L);
+        outer.putDouble("double", 44d);
+        outer.putString("string", "com.example <and></and> &amp; more");
+
+        outer.putBooleanArray("boolean[]", new boolean[] { true, false, true });
+        outer.putIntArray("int[]", new int[] { 42, 43, 44 });
+        outer.putLongArray("long[]", new long[] { 43L, 44L, 45L });
+        outer.putDoubleArray("double[]", new double[] { 43d, 44d, 45d });
+        outer.putStringArray("string[]", new String[] { "foo", "bar", "baz" });
+
+        final PersistableBundle nested = new PersistableBundle();
+        nested.putString("nested_key", "nested_value");
+        outer.putPersistableBundle("nested", nested);
+
+        return outer;
+    }
+
+    private 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
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        out.setOutput(os, StandardCharsets.UTF_8.name());
+        out.startTag(null, "bundle");
+        bundle.saveToXml(out);
+        out.endTag(null, "bundle");
+        out.flush();
+        return os.toByteArray();
+    }
+
+    private static PersistableBundle doPersistableBundleRead(TypedXmlPullParser in, byte[] raw)
+            throws Exception {
+        final ByteArrayInputStream is = new ByteArrayInputStream(raw);
+        in.setInput(is, StandardCharsets.UTF_8.name());
+        in.next();
+        return PersistableBundle.restoreFromXml(in);
+    }
+
+    @Test
+    public void testVerify_Normal() throws Exception {
+        doVerify(XmlUtils.makeTyped(Xml.newSerializer()),
+                XmlUtils.makeTyped(Xml.newPullParser()));
+    }
+
+    @Test
+    public void testVerify_Fast() throws Exception {
+        doVerify(Xml.newFastSerializer(),
+                Xml.newFastPullParser());
+    }
+
+    /**
+     * Verify that example test data is correctly serialized and parsed
+     * end-to-end using the given objects.
+     */
+    private static void doVerify(TypedXmlSerializer out, TypedXmlPullParser in) throws Exception {
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        out.setOutput(os, StandardCharsets.UTF_8.name());
+        doVerifyWrite(out);
+        out.flush();
+
+        final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
+        in.setInput(is, StandardCharsets.UTF_8.name());
+        doVerifyRead(in);
+    }
+
+    private static final String TEST_STRING = "com.example";
+    private static final byte[] TEST_BYTES = new byte[] { 0, 1, 2, 3, 4, 3, 2, 1, 0 };
+
+    private static void doVerifyWrite(TypedXmlSerializer out) throws Exception {
+        out.startDocument(StandardCharsets.UTF_8.name(), true);
+        out.startTag(null, "one");
+        {
+            out.startTag(null, "two");
+            {
+                out.attribute(null, "string", TEST_STRING);
+                out.attribute(null, "stringNumber", "49");
+                out.attributeBytesHex(null, "bytesHex", TEST_BYTES);
+                out.attributeBytesBase64(null, "bytesBase64", TEST_BYTES);
+                out.attributeInt(null, "int", 43);
+                out.attributeIntHex(null, "intHex", 44);
+                out.attributeLong(null, "long", 45L);
+                out.attributeLongHex(null, "longHex", 46L);
+                out.attributeFloat(null, "float", 47f);
+                out.attributeDouble(null, "double", 48d);
+                out.attributeBoolean(null, "boolean", true);
+            }
+            out.endTag(null, "two");
+
+            out.startTag(null, "three");
+            {
+                out.text("foo");
+                out.startTag(null, "four");
+                {
+                }
+                out.endTag(null, "four");
+                out.text("bar");
+                out.text("baz");
+            }
+            out.endTag(null, "three");
+        }
+        out.endTag(null, "one");
+        out.endDocument();
+    }
+
+    private static void doVerifyRead(TypedXmlPullParser in) throws Exception {
+        assertEquals(START_DOCUMENT, in.getEventType());
+        assertNext(in, START_TAG, "one", 1);
+        {
+            assertNext(in, START_TAG, "two", 2);
+            {
+                assertEquals(11, in.getAttributeCount());
+                assertEquals(TEST_STRING, in.getAttributeValue(null, "string"));
+                assertArrayEquals(TEST_BYTES, in.getAttributeBytesHex(null, "bytesHex"));
+                assertArrayEquals(TEST_BYTES, in.getAttributeBytesBase64(null, "bytesBase64"));
+                assertEquals(43, in.getAttributeInt(null, "int"));
+                assertEquals(44, in.getAttributeIntHex(null, "intHex"));
+                assertEquals(45L, in.getAttributeLong(null, "long"));
+                assertEquals(46L, in.getAttributeLongHex(null, "longHex"));
+                assertEquals(47f, in.getAttributeFloat(null, "float"), 0.01);
+                assertEquals(48d, in.getAttributeDouble(null, "double"), 0.01);
+                assertEquals(true, in.getAttributeBoolean(null, "boolean"));
+
+                // Also verify that typed values are available as strings
+                assertEquals("000102030403020100", in.getAttributeValue(null, "bytesHex"));
+                assertEquals("AAECAwQDAgEA", in.getAttributeValue(null, "bytesBase64"));
+                assertEquals("43", in.getAttributeValue(null, "int"));
+                assertEquals("2c", in.getAttributeValue(null, "intHex"));
+                assertEquals("45", in.getAttributeValue(null, "long"));
+                assertEquals("2e", in.getAttributeValue(null, "longHex"));
+                assertEquals("true", in.getAttributeValue(null, "boolean"));
+
+                // And that raw strings can be parsed too
+                assertEquals("49", in.getAttributeValue(null, "stringNumber"));
+                assertEquals(49, in.getAttributeInt(null, "stringNumber"));
+            }
+            assertNext(in, END_TAG, "two", 2);
+
+            assertNext(in, START_TAG, "three", 2);
+            {
+                assertNext(in, TEXT);
+                assertEquals("foo", in.getText().trim());
+                assertNext(in, START_TAG, "four", 3);
+                {
+                    assertEquals(0, in.getAttributeCount());
+                }
+                assertNext(in, END_TAG, "four", 3);
+                assertNext(in, TEXT);
+                assertEquals("barbaz", in.getText().trim());
+            }
+            assertNext(in, END_TAG, "three", 2);
+        }
+        assertNext(in, END_TAG, "one", 1);
+        assertNext(in, END_DOCUMENT);
+    }
+
+    private 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;
+        while ((event = in.next()) == TEXT && in.getText().trim().length() == 0) {
+        }
+        assertEquals("next", token, event);
+        assertEquals("getEventType", token, in.getEventType());
+    }
+
+    private static void assertNext(TypedXmlPullParser in, int token, String name, int depth)
+            throws Exception {
+        assertNext(in, token);
+        assertEquals("getName", name, in.getName());
+        assertEquals("getDepth", depth, in.getDepth());
+    }
+}