ASL command-line tool initial implementation

Includes logic for converting data types from HR to OD XML format, as well as storing them in an internal Java representation. Future CLs will implement more fields, make error checking more robust, and add tests.

Bug: 287487923
Test: TODO in future CLs
Change-Id: I6170feec9df0ce709b912d46356204badacfbe5b
diff --git a/tools/app_metadata_bundles/Android.bp b/tools/app_metadata_bundles/Android.bp
index 5f7589d..be6bea6 100644
--- a/tools/app_metadata_bundles/Android.bp
+++ b/tools/app_metadata_bundles/Android.bp
@@ -12,11 +12,6 @@
     srcs: [
         "src/lib/java/**/*.java",
     ],
-    target: {
-        windows: {
-            enabled: true,
-        },
-    },
 }
 
 java_binary_host {
diff --git a/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java
index bf906ee..df003b6 100644
--- a/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java
+++ b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java
@@ -19,16 +19,20 @@
 import com.android.asllib.AndroidSafetyLabel;
 import com.android.asllib.AndroidSafetyLabel.Format;
 
+import org.xml.sax.SAXException;
+
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+
 public class Main {
 
-    /**
-     * Takes the options to make file conversion.
-     */
-    public static void main(String[] args) throws IOException {
+    /** Takes the options to make file conversion. */
+    public static void main(String[] args)
+            throws IOException, ParserConfigurationException, SAXException, TransformerException {
 
         String inFile = null;
         String outFile = null;
@@ -78,15 +82,13 @@
             throw new IllegalArgumentException("output format is required");
         }
 
-
         System.out.println("in path: " + inFile);
         System.out.println("out path: " + outFile);
         System.out.println("in format: " + inFormat);
         System.out.println("out format: " + outFormat);
 
-        var asl = AndroidSafetyLabel.readFromStream(new FileInputStream(inFile),
-                Format.HUMAN_READABLE);
-        asl.writeToStream(new FileOutputStream(outFile), Format.ON_DEVICE);
+        var asl = AndroidSafetyLabel.readFromStream(new FileInputStream(inFile), inFormat);
+        asl.writeToStream(new FileOutputStream(outFile), outFormat);
     }
 
     private static Format getFormat(String argValue) {
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
index 0d13a0f..07e0e73 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
@@ -16,13 +16,22 @@
 
 package com.android.asllib;
 
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.io.OutputStreamWriter;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
 
 public class AndroidSafetyLabel {
 
@@ -30,30 +39,64 @@
         NULL, HUMAN_READABLE, ON_DEVICE;
     }
 
-    /**
-     * Reads a {@link AndroidSafetyLabel} from an {@link InputStream}.
-     */
-    public static AndroidSafetyLabel readFromStream(InputStream in, Format format)
-            throws IOException {
-        System.out.println(format);
-        var br = new BufferedReader(new InputStreamReader(in));
-        String line;
-        while ((line = br.readLine()) != null) {
-            System.out.println(line);
-        }
-        return new AndroidSafetyLabel();
+    private final SafetyLabels mSafetyLabels;
+
+    public SafetyLabels getSafetyLabels() {
+        return mSafetyLabels;
     }
 
-    /**
-     * Write the content of the {@link AndroidSafetyLabel} to a {@link OutputStream}.
-     */
-    public void writeToStream(OutputStream out, Format format) throws IOException {
-        var bw = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
-        bw.write("Just a Test");
-        bw.close();
+    private AndroidSafetyLabel(SafetyLabels safetyLabels) {
+        this.mSafetyLabels = safetyLabels;
+    }
+
+    /** Reads a {@link AndroidSafetyLabel} from an {@link InputStream}. */
+    // TODO(b/329902686): Support conversion in both directions, specified by format.
+    public static AndroidSafetyLabel readFromStream(InputStream in, Format format)
+            throws IOException, ParserConfigurationException, SAXException {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        Document document = factory.newDocumentBuilder().parse(in);
+
+        Element appMetadataBundles =
+                XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES);
+
+        return AndroidSafetyLabel.createFromHrElement(appMetadataBundles);
+    }
+
+    /** Write the content of the {@link AndroidSafetyLabel} to a {@link OutputStream}. */
+    // TODO(b/329902686): Support conversion in both directions, specified by format.
+    public void writeToStream(OutputStream out, Format format)
+            throws IOException, ParserConfigurationException, TransformerException {
+        var docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        var document = docBuilder.newDocument();
+        document.appendChild(this.toOdDomElement(document));
+
+        TransformerFactory transformerFactory = TransformerFactory.newInstance();
+        Transformer transformer = transformerFactory.newTransformer();
+        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+        StreamResult streamResult = new StreamResult(out); // out
+        DOMSource domSource = new DOMSource(document);
+        transformer.transform(domSource, streamResult);
+    }
+
+    /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */
+    public static AndroidSafetyLabel createFromHrElement(Element appMetadataBundlesEle) {
+        Element safetyLabelsEle =
+                XmlUtils.getSingleElement(appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS);
+        SafetyLabels safetyLabels = SafetyLabels.createFromHrElement(safetyLabelsEle);
+        return new AndroidSafetyLabel(safetyLabels);
+    }
+
+    /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */
+    public Element toOdDomElement(Document doc) {
+        Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE);
+        aslEle.appendChild(mSafetyLabels.toOdDomElement(doc));
+        return aslEle;
     }
 
     public static void test() {
-        System.out.println("test lib");
+        // TODO(b/329902686): Add tests.
     }
 }
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
index 35ec68d..efdaa40 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
@@ -35,4 +35,9 @@
     public Map<String, DataType> getDataTypes() {
         return mDataTypes;
     }
+
+    /** Creates a {@link DataCategory} given map of {@param dataTypes}. */
+    public static DataCategory create(Map<String, DataType> dataTypes) {
+        return new DataCategory(dataTypes);
+    }
 }
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
index 1925c28..b364c8b 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
@@ -23,7 +23,7 @@
 import java.util.Set;
 
 /**
- * Constants for determining valid {@link String} data types for usage within {@link SafetyLabel},
+ * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels},
  * {@link DataCategory}, and {@link DataType}
  */
 public class DataCategoryConstants {
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabel.java
deleted file mode 100644
index dc8f5cc2..0000000
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabel.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2024 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.asllib;
-
-import java.util.Map;
-
-/**
- * Data label representation with data shared and data collected maps containing zero or more
- * {@link DataCategory}
- */
-public class DataLabel {
-    private final Map<String, DataCategory> mDataAccessed;
-    private final Map<String, DataCategory> mDataCollected;
-    private final Map<String, DataCategory> mDataShared;
-
-    public DataLabel(
-            Map<String, DataCategory> dataAccessed,
-            Map<String, DataCategory> dataCollected,
-            Map<String, DataCategory> dataShared) {
-        mDataAccessed = dataAccessed;
-        mDataCollected = dataCollected;
-        mDataShared = dataShared;
-    }
-
-    /**
-     * Returns the data accessed {@link Map} of {@link
-     * com.android.asllib.DataCategoryConstants} to {@link DataCategory}
-     */
-    public Map<String, DataCategory> getDataAccessed() {
-        return mDataAccessed;
-    }
-
-    /**
-     * Returns the data collected {@link Map} of {@link
-     * com.android.asllib.DataCategoryConstants} to {@link DataCategory}
-     */
-    public Map<String, DataCategory> getDataCollected() {
-        return mDataCollected;
-    }
-
-    /**
-     * Returns the data shared {@link Map} of {@link
-     * com.android.asllib.DataCategoryConstants} to {@link DataCategory}
-     */
-    public Map<String, DataCategory> getDataShared() {
-        return mDataShared;
-    }
-}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
new file mode 100644
index 0000000..d2c3d75b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2024 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Data label representation with data shared and data collected maps containing zero or more {@link
+ * DataCategory}
+ */
+public class DataLabels {
+    private final Map<String, DataCategory> mDataAccessed;
+    private final Map<String, DataCategory> mDataCollected;
+    private final Map<String, DataCategory> mDataShared;
+
+    public DataLabels(
+            Map<String, DataCategory> dataAccessed,
+            Map<String, DataCategory> dataCollected,
+            Map<String, DataCategory> dataShared) {
+        mDataAccessed = dataAccessed;
+        mDataCollected = dataCollected;
+        mDataShared = dataShared;
+    }
+
+    /**
+     * Returns the data accessed {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+     * {@link DataCategory}
+     */
+    public Map<String, DataCategory> getDataAccessed() {
+        return mDataAccessed;
+    }
+
+    /**
+     * Returns the data collected {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+     * {@link DataCategory}
+     */
+    public Map<String, DataCategory> getDataCollected() {
+        return mDataCollected;
+    }
+
+    /**
+     * Returns the data shared {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+     * {@link DataCategory}
+     */
+    public Map<String, DataCategory> getDataShared() {
+        return mDataShared;
+    }
+
+    /** Creates a {@link DataLabels} from the human-readable DOM element. */
+    public static DataLabels createFromHrElement(Element ele) {
+        Map<String, DataCategory> dataAccessed =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_ACCESSED);
+        Map<String, DataCategory> dataCollected =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_COLLECTED);
+        Map<String, DataCategory> dataShared =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_SHARED);
+        return new DataLabels(dataAccessed, dataCollected, dataShared);
+    }
+
+    private static Map<String, DataCategory> getDataCategoriesWithTag(
+            Element dataLabelsEle, String dataCategoryUsageTypeTag) {
+        Map<String, Map<String, DataType>> dataTypeMap =
+                new HashMap<String, Map<String, DataType>>();
+        NodeList dataSharedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag);
+
+        for (int i = 0; i < dataSharedNodeList.getLength(); i++) {
+            Element dataSharedEle = (Element) dataSharedNodeList.item(i);
+            String dataCategoryName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
+            String dataTypeName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
+
+            if (!dataTypeMap.containsKey((dataCategoryName))) {
+                dataTypeMap.put(dataCategoryName, new HashMap<String, DataType>());
+            }
+            dataTypeMap
+                    .get(dataCategoryName)
+                    .put(dataTypeName, DataType.createFromHrElement(dataSharedEle));
+        }
+
+        Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>();
+        for (String dataCategoryName : dataTypeMap.keySet()) {
+            Map<String, DataType> dataTypes = dataTypeMap.get(dataCategoryName);
+            dataCategoryMap.put(dataCategoryName, DataCategory.create(dataTypes));
+        }
+        return dataCategoryMap;
+    }
+
+    /** Gets the on-device DOM element for the {@link DataLabels}. */
+    public Element toOdDomElement(Document doc) {
+        Element dataLabelsEle =
+                XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS);
+
+        maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_ACCESSED);
+        maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED);
+        maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED);
+
+        return dataLabelsEle;
+    }
+
+    private void maybeAppendDataUsages(
+            Document doc,
+            Element dataLabelsEle,
+            Map<String, DataCategory> dataCategoriesMap,
+            String dataUsageTypeName) {
+        if (dataCategoriesMap.isEmpty()) {
+            return;
+        }
+        Element dataUsageEle = XmlUtils.createPbundleEleWithName(doc, dataUsageTypeName);
+
+        for (String dataCategoryName : dataCategoriesMap.keySet()) {
+            Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, dataCategoryName);
+            DataCategory dataCategory = dataCategoriesMap.get(dataCategoryName);
+            for (String dataTypeName : dataCategory.getDataTypes().keySet()) {
+                DataType dataType = dataCategory.getDataTypes().get(dataTypeName);
+                Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, dataTypeName);
+                if (!dataType.getPurposeSet().isEmpty()) {
+                    Element purposesEle = doc.createElement(XmlUtils.OD_TAG_INT_ARRAY);
+                    purposesEle.setAttribute(XmlUtils.OD_ATTR_NAME, XmlUtils.OD_NAME_PURPOSES);
+                    purposesEle.setAttribute(
+                            XmlUtils.OD_ATTR_NUM, String.valueOf(dataType.getPurposeSet().size()));
+                    for (DataType.Purpose purpose : dataType.getPurposeSet()) {
+                        Element purposeEle = doc.createElement(XmlUtils.OD_TAG_ITEM);
+                        purposeEle.setAttribute(
+                                XmlUtils.OD_ATTR_VALUE, String.valueOf(purpose.getValue()));
+                        purposesEle.appendChild(purposeEle);
+                    }
+                    dataTypeEle.appendChild(purposesEle);
+                }
+
+                maybeAddBoolToOdElement(
+                        doc,
+                        dataTypeEle,
+                        dataType.getIsCollectionOptional(),
+                        XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL);
+                maybeAddBoolToOdElement(
+                        doc,
+                        dataTypeEle,
+                        dataType.getIsSharingOptional(),
+                        XmlUtils.OD_NAME_IS_SHARING_OPTIONAL);
+                maybeAddBoolToOdElement(
+                        doc, dataTypeEle, dataType.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL);
+
+                dataCategoryEle.appendChild(dataTypeEle);
+            }
+            dataUsageEle.appendChild(dataCategoryEle);
+        }
+        dataLabelsEle.appendChild(dataUsageEle);
+    }
+
+    private static void maybeAddBoolToOdElement(
+            Document doc, Element parentEle, Boolean b, String odName) {
+        if (b == null) {
+            return;
+        }
+        Element ele = XmlUtils.createOdBooleanEle(doc, odName, b);
+        parentEle.appendChild(ele);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
index 1601d15..7451c69 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
@@ -16,21 +16,75 @@
 
 package com.android.asllib;
 
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Data usage type representation. Types are specific to a {@link DataCategory} and contains
  * metadata related to the data usage purpose.
  */
 public class DataType {
+    public enum Purpose {
+        PURPOSE_APP_FUNCTIONALITY(1),
+        PURPOSE_ANALYTICS(2),
+        PURPOSE_DEVELOPER_COMMUNICATIONS(3),
+        PURPOSE_FRAUD_PREVENTION_SECURITY(4),
+        PURPOSE_ADVERTISING(5),
+        PURPOSE_PERSONALIZATION(6),
+        PURPOSE_ACCOUNT_MANAGEMENT(7);
 
-    private final Set<Integer> mPurposeSet;
+        private static final String PURPOSE_PREFIX = "PURPOSE_";
+
+        private final int mValue;
+
+        Purpose(int value) {
+            this.mValue = value;
+        }
+
+        /** Get the int value associated with the Purpose. */
+        public int getValue() {
+            return mValue;
+        }
+
+        /** Get the Purpose associated with the int value. */
+        public static Purpose forValue(int value) {
+            for (Purpose e : values()) {
+                if (e.getValue() == value) {
+                    return e;
+                }
+            }
+            throw new IllegalArgumentException("No enum for value: " + value);
+        }
+
+        /** Get the Purpose associated with the human-readable String. */
+        public static Purpose forString(String s) {
+            for (Purpose e : values()) {
+                if (e.toString().equals(s)) {
+                    return e;
+                }
+            }
+            throw new IllegalArgumentException("No enum for str: " + s);
+        }
+
+        /** Human-readable String representation of Purpose. */
+        public String toString() {
+            if (!this.name().startsWith(PURPOSE_PREFIX)) {
+                return this.name();
+            }
+            return this.name().substring(PURPOSE_PREFIX.length()).toLowerCase();
+        }
+    }
+
+    private final Set<Purpose> mPurposeSet;
     private final Boolean mIsCollectionOptional;
     private final Boolean mIsSharingOptional;
     private final Boolean mEphemeral;
 
     private DataType(
-            Set<Integer> purposeSet,
+            Set<Purpose> purposeSet,
             Boolean isCollectionOptional,
             Boolean isSharingOptional,
             Boolean ephemeral) {
@@ -44,7 +98,7 @@
      * Returns {@link Set} of valid {@link Integer} purposes for using the associated data category
      * and type
      */
-    public Set<Integer> getPurposeSet() {
+    public Set<Purpose> getPurposeSet() {
         return mPurposeSet;
     }
 
@@ -71,5 +125,21 @@
     public Boolean getEphemeral() {
         return mEphemeral;
     }
-}
 
+    /** Creates a {@link DataType} from the human-readable DOM element. */
+    public static DataType createFromHrElement(Element hrDataTypeEle) {
+        Set<Purpose> purposeSet =
+                Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|"))
+                        .map(Purpose::forString)
+                        .collect(Collectors.toUnmodifiableSet());
+        Boolean isCollectionOptional =
+                XmlUtils.fromString(
+                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL));
+        Boolean isSharingOptional =
+                XmlUtils.fromString(
+                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL));
+        Boolean ephemeral =
+                XmlUtils.fromString(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_EPHEMERAL));
+        return new DataType(purposeSet, isCollectionOptional, isSharingOptional, ephemeral);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java
new file mode 100644
index 0000000..a0a7537
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 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.asllib;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels},
+ * {@link DataCategory}, and {@link DataType}
+ */
+public class DataTypeConstants {
+    /** Data types for {@link DataCategoryConstants.CATEGORY_PERSONAL} */
+    public static final String TYPE_NAME = "name";
+
+    public static final String TYPE_EMAIL_ADDRESS = "email_address";
+    public static final String TYPE_PHONE_NUMBER = "phone_number";
+    public static final String TYPE_RACE_ETHNICITY = "race_ethnicity";
+    public static final String TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS =
+            "political_or_religious_beliefs";
+    public static final String TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY =
+            "sexual_orientation_or_gender_identity";
+    public static final String TYPE_PERSONAL_IDENTIFIERS = "personal_identifiers";
+    public static final String TYPE_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_FINANCIAL} */
+    public static final String TYPE_CARD_BANK_ACCOUNT = "card_bank_account";
+
+    public static final String TYPE_PURCHASE_HISTORY = "purchase_history";
+    public static final String TYPE_CREDIT_SCORE = "credit_score";
+    public static final String TYPE_FINANCIAL_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_LOCATION} */
+    public static final String TYPE_APPROX_LOCATION = "approx_location";
+
+    public static final String TYPE_PRECISE_LOCATION = "precise_location";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_EMAIL_TEXT_MESSAGE} */
+    public static final String TYPE_EMAILS = "emails";
+
+    public static final String TYPE_TEXT_MESSAGES = "text_messages";
+    public static final String TYPE_EMAIL_TEXT_MESSAGE_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_PHOTO_VIDEO} */
+    public static final String TYPE_PHOTOS = "photos";
+
+    public static final String TYPE_VIDEOS = "videos";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_AUDIO} */
+    public static final String TYPE_SOUND_RECORDINGS = "sound_recordings";
+
+    public static final String TYPE_MUSIC_FILES = "music_files";
+    public static final String TYPE_AUDIO_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_STORAGE} */
+    public static final String TYPE_FILES_DOCS = "files_docs";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_HEALTH_FITNESS} */
+    public static final String TYPE_HEALTH = "health";
+
+    public static final String TYPE_FITNESS = "fitness";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_CONTACTS} */
+    public static final String TYPE_CONTACTS = "contacts";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_CALENDAR} */
+    public static final String TYPE_CALENDAR = "calendar";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_IDENTIFIERS} */
+    public static final String TYPE_IDENTIFIERS_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_APP_PERFORMANCE} */
+    public static final String TYPE_CRASH_LOGS = "crash_logs";
+
+    public static final String TYPE_PERFORMANCE_DIAGNOSTICS = "performance_diagnostics";
+    public static final String TYPE_APP_PERFORMANCE_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_ACTIONS_IN_APP} */
+    public static final String TYPE_USER_INTERACTION = "user_interaction";
+
+    public static final String TYPE_IN_APP_SEARCH_HISTORY = "in_app_search_history";
+    public static final String TYPE_INSTALLED_APPS = "installed_apps";
+    public static final String TYPE_USER_GENERATED_CONTENT = "user_generated_content";
+    public static final String TYPE_ACTIONS_IN_APP_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_SEARCH_AND_BROWSING} */
+    public static final String TYPE_WEB_BROWSING_HISTORY = "web_browsing_history";
+
+    /** Set of valid categories */
+    public static final Set<String> VALID_TYPES =
+            Collections.unmodifiableSet(
+                    new HashSet<>(
+                            Arrays.asList(
+                                    TYPE_NAME,
+                                    TYPE_EMAIL_ADDRESS,
+                                    TYPE_PHONE_NUMBER,
+                                    TYPE_RACE_ETHNICITY,
+                                    TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS,
+                                    TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY,
+                                    TYPE_PERSONAL_IDENTIFIERS,
+                                    TYPE_OTHER,
+                                    TYPE_CARD_BANK_ACCOUNT,
+                                    TYPE_PURCHASE_HISTORY,
+                                    TYPE_CREDIT_SCORE,
+                                    TYPE_FINANCIAL_OTHER,
+                                    TYPE_APPROX_LOCATION,
+                                    TYPE_PRECISE_LOCATION,
+                                    TYPE_EMAILS,
+                                    TYPE_TEXT_MESSAGES,
+                                    TYPE_EMAIL_TEXT_MESSAGE_OTHER,
+                                    TYPE_PHOTOS,
+                                    TYPE_VIDEOS,
+                                    TYPE_SOUND_RECORDINGS,
+                                    TYPE_MUSIC_FILES,
+                                    TYPE_AUDIO_OTHER,
+                                    TYPE_FILES_DOCS,
+                                    TYPE_HEALTH,
+                                    TYPE_FITNESS,
+                                    TYPE_CONTACTS,
+                                    TYPE_CALENDAR,
+                                    TYPE_IDENTIFIERS_OTHER,
+                                    TYPE_CRASH_LOGS,
+                                    TYPE_PERFORMANCE_DIAGNOSTICS,
+                                    TYPE_APP_PERFORMANCE_OTHER,
+                                    TYPE_USER_INTERACTION,
+                                    TYPE_IN_APP_SEARCH_HISTORY,
+                                    TYPE_INSTALLED_APPS,
+                                    TYPE_USER_GENERATED_CONTENT,
+                                    TYPE_ACTIONS_IN_APP_OTHER,
+                                    TYPE_WEB_BROWSING_HISTORY)));
+
+    /** Returns {@link Set} of valid {@link String} category keys */
+    public static Set<String> getValidDataTypes() {
+        return VALID_TYPES;
+    }
+
+    private DataTypeConstants() {
+        /* do nothing - hide constructor */
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabel.java
deleted file mode 100644
index 8d8f0bb..0000000
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabel.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2024 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.asllib;
-
-
-/** Safety Label representation containing zero or more {@link DataCategory} for data shared */
-public class SafetyLabel {
-
-    private final long mVersion;
-    private final DataLabel mDataLabel;
-
-    private SafetyLabel(long version, DataLabel dataLabel) {
-        this.mVersion = version;
-        this.mDataLabel = dataLabel;
-    }
-
-    /** Returns the data label for the safety label */
-    public DataLabel getDataLabel() {
-        return mDataLabel;
-    }
-
-    public long getVersion() {
-        return mVersion;
-    }
-}
-
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
new file mode 100644
index 0000000..6ba15e1
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+/** Safety Label representation containing zero or more {@link DataCategory} for data shared */
+public class SafetyLabels {
+
+    private final Long mVersion;
+    private final DataLabels mDataLabels;
+
+    private SafetyLabels(Long version, DataLabels dataLabels) {
+        this.mVersion = version;
+        this.mDataLabels = dataLabels;
+    }
+
+    /** Returns the data label for the safety label */
+    public DataLabels getDataLabel() {
+        return mDataLabels;
+    }
+
+    /** Gets the version of the {@link SafetyLabels}. */
+    public Long getVersion() {
+        return mVersion;
+    }
+
+    /** Creates a {@link SafetyLabels} from the human-readable DOM element. */
+    public static SafetyLabels createFromHrElement(Element safetyLabelsEle) {
+        Long version;
+        try {
+            version = Long.parseLong(safetyLabelsEle.getAttribute(XmlUtils.HR_ATTR_VERSION));
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Malformed or missing required version in safety labels.");
+        }
+        Element dataLabelsEle =
+                XmlUtils.getSingleElement(safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS);
+        DataLabels dataLabels = DataLabels.createFromHrElement(dataLabelsEle);
+        return new SafetyLabels(version, dataLabels);
+    }
+
+    /** Creates an on-device DOM element from the {@link SafetyLabels}. */
+    public Element toOdDomElement(Document doc) {
+        Element safetyLabelsEle =
+                XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS);
+        safetyLabelsEle.appendChild(mDataLabels.toOdDomElement(doc));
+        return safetyLabelsEle;
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
new file mode 100644
index 0000000..4392c2c
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+public class XmlUtils {
+    public static final String HR_TAG_APP_METADATA_BUNDLES = "app-metadata-bundles";
+    public static final String HR_TAG_SAFETY_LABELS = "safety-labels";
+    public static final String HR_TAG_DATA_LABELS = "data-labels";
+    public static final String HR_TAG_DATA_ACCESSED = "data-accessed";
+    public static final String HR_TAG_DATA_COLLECTED = "data-collected";
+    public static final String HR_TAG_DATA_SHARED = "data-shared";
+
+    public static final String HR_ATTR_DATA_CATEGORY = "dataCategory";
+    public static final String HR_ATTR_DATA_TYPE = "dataType";
+    public static final String HR_ATTR_IS_COLLECTION_OPTIONAL = "isCollectionOptional";
+    public static final String HR_ATTR_IS_SHARING_OPTIONAL = "isSharingOptional";
+    public static final String HR_ATTR_EPHEMERAL = "ephemeral";
+    public static final String HR_ATTR_PURPOSES = "purposes";
+    public static final String HR_ATTR_VERSION = "version";
+
+    public static final String OD_TAG_BUNDLE = "bundle";
+    public static final String OD_TAG_PBUNDLE_AS_MAP = "pbundle_as_map";
+    public static final String OD_TAG_BOOLEAN = "boolean";
+    public static final String OD_TAG_INT_ARRAY = "int-array";
+    public static final String OD_TAG_ITEM = "item";
+    public static final String OD_ATTR_NAME = "name";
+    public static final String OD_ATTR_VALUE = "value";
+    public static final String OD_ATTR_NUM = "num";
+    public static final String OD_NAME_SAFETY_LABELS = "safety_labels";
+    public static final String OD_NAME_DATA_LABELS = "data_labels";
+    public static final String OD_NAME_DATA_ACCESSED = "data_accessed";
+    public static final String OD_NAME_DATA_COLLECTED = "data_collected";
+    public static final String OD_NAME_DATA_SHARED = "data_shared";
+    public static final String OD_NAME_PURPOSES = "purposes";
+    public static final String OD_NAME_IS_COLLECTION_OPTIONAL = "is_collection_optional";
+    public static final String OD_NAME_IS_SHARING_OPTIONAL = "is_sharing_optional";
+    public static final String OD_NAME_EPHEMERAL = "ephemeral";
+
+    public static final String TRUE_STR = "true";
+    public static final String FALSE_STR = "false";
+
+    /** Gets the single top-level {@link Element} having the {@param tagName}. */
+    public static Element getSingleElement(Document doc, String tagName) {
+        var elements = doc.getElementsByTagName(tagName);
+        return getSingleElement(elements, tagName);
+    }
+
+    /**
+     * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
+     */
+    public static Element getSingleElement(Element parentEle, String tagName) {
+        var elements = parentEle.getElementsByTagName(tagName);
+        return getSingleElement(elements, tagName);
+    }
+
+    /** Gets the single {@link Element} from {@param elements} and having the {@param tagName}. */
+    public static Element getSingleElement(NodeList elements, String tagName) {
+        if (elements.getLength() != 1) {
+            throw new IllegalArgumentException(
+                    String.format("Expected 1 %s but got %s.", tagName, elements.getLength()));
+        }
+        var elementAsNode = elements.item(0);
+        if (!(elementAsNode instanceof Element)) {
+            throw new IllegalStateException(String.format("%s was not an element.", tagName));
+        }
+        return ((Element) elementAsNode);
+    }
+
+    /** Gets the Boolean from the String value. */
+    public static Boolean fromString(String s) {
+        if (s == null) {
+            return null;
+        }
+        if (s.equals(TRUE_STR)) {
+            return true;
+        } else if (s.equals(FALSE_STR)) {
+            return false;
+        }
+        return null;
+    }
+
+    /** Creates an on-device PBundle DOM Element with the given attribute name. */
+    public static Element createPbundleEleWithName(Document doc, String name) {
+        var ele = doc.createElement(XmlUtils.OD_TAG_PBUNDLE_AS_MAP);
+        ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
+        return ele;
+    }
+
+    /** Create an on-device Boolean DOM Element with the given attribute name. */
+    public static Element createOdBooleanEle(Document doc, String name, boolean b) {
+        var ele = doc.createElement(XmlUtils.OD_TAG_BOOLEAN);
+        ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
+        ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(b));
+        return ele;
+    }
+
+    /** Returns whether the String is null or empty. */
+    public static boolean isNullOrEmpty(String s) {
+        return s == null || s.isEmpty();
+    }
+}