Snap for 9068117 from 7d360b3bc2e2ed19e5d7f64fe2e2b20e2dbcdee9 to mainline-neuralnetworks-release

Change-Id: Icc8fddaa768e4d1993f820a5b5fba1d07419f415
diff --git a/libnativeloader/library_namespaces.cpp b/libnativeloader/library_namespaces.cpp
index 96d4dde..f3c93a0 100644
--- a/libnativeloader/library_namespaces.cpp
+++ b/libnativeloader/library_namespaces.cpp
@@ -81,16 +81,22 @@
 constexpr const char* kAlwaysPermittedDirectories = "/data:/mnt/expand";
 
 constexpr const char* kVendorLibPath = "/vendor/" LIB;
+// TODO(mast): It's unlikely that both paths are necessary for kProductLibPath
+// below, because they can't be two separate directories - either one has to be
+// a symlink to the other.
 constexpr const char* kProductLibPath = "/product/" LIB ":/system/product/" LIB;
+constexpr const char* kSystemLibPath = "/system/" LIB ":/system_ext/" LIB;
 
-const std::regex kVendorDexPathRegex("(^|:)/vendor/");
+const std::regex kVendorDexPathRegex("(^|:)(/system)?/vendor/");
 const std::regex kProductDexPathRegex("(^|:)(/system)?/product/");
+const std::regex kSystemDexPathRegex("(^|:)/system(_ext)?/");  // MUST be tested last.
 
-// Define origin of APK if it is from vendor partition or product partition
+// Define origin partition of APK
 using ApkOrigin = enum {
   APK_ORIGIN_DEFAULT = 0,
-  APK_ORIGIN_VENDOR = 1,
-  APK_ORIGIN_PRODUCT = 2,
+  APK_ORIGIN_VENDOR = 1,   // Includes both /vendor and /system/vendor
+  APK_ORIGIN_PRODUCT = 2,  // Includes both /product and /system/product
+  APK_ORIGIN_SYSTEM = 3,   // Includes both /system and /system_ext but not /system/{vendor,product}
 };
 
 jobject GetParentClassLoader(JNIEnv* env, jobject class_loader) {
@@ -113,6 +119,9 @@
 
     apk_origin = APK_ORIGIN_PRODUCT;
   }
+  if (apk_origin == APK_ORIGIN_DEFAULT && std::regex_search(dex_path, kSystemDexPathRegex)) {
+    apk_origin = APK_ORIGIN_SYSTEM;
+  }
   return apk_origin;
 }
 
@@ -234,7 +243,18 @@
   const char* apk_origin_msg = "other apk";  // Only for debug logging.
 
   if (!is_shared) {
-    if (apk_origin == APK_ORIGIN_VENDOR) {
+    if (apk_origin == APK_ORIGIN_SYSTEM) {
+      // System apps commonly get shared namespaces and hence don't need this.
+      // In practice it's necessary for shared system libraries (i.e. JARs
+      // rather than actual APKs) that are loaded by ordinary apps which don't
+      // get shared namespaces.
+      apk_origin_msg = "system apk";
+
+      // Give access to all libraries in the system and system_ext partitions
+      // (they can freely access each other's private APIs).
+      library_path = library_path + ":" + kSystemLibPath;
+      permitted_path = permitted_path + ":" + kSystemLibPath;
+    } else if (apk_origin == APK_ORIGIN_VENDOR) {
       unbundled_app_origin = APK_ORIGIN_VENDOR;
       apk_origin_msg = "unbundled vendor apk";
 
@@ -288,8 +308,7 @@
     // they are to other apps, including those in system, system_ext, and
     // product partitions. The reason is that when GSI is used, the system
     // partition may get replaced, and then vendor apps may fail. It's fine for
-    // product (and system_ext) apps, because those partitions aren't mounted in
-    // GSI tests.
+    // product apps, because that partition isn't mounted in GSI tests.
     auto libs =
         filter_public_libraries(target_sdk_version, uses_libraries, extended_public_libraries());
     if (!libs.empty()) {
diff --git a/libnativeloader/test/Android.bp b/libnativeloader/test/Android.bp
index dd5e969..1d3a07a 100644
--- a/libnativeloader/test/Android.bp
+++ b/libnativeloader/test/Android.bp
@@ -30,8 +30,7 @@
 }
 
 // This app is just an intermediate container to be able to include the .so
-// library as a java resource in the host test. It's not actually installed or
-// started.
+// library in the host test. It's not actually installed or started.
 android_test_helper_app {
     name: "library_container_app",
     defaults: ["art_module_source_build_java_defaults"],
@@ -40,19 +39,40 @@
     jni_libs: ["libnativeloader_testlib"],
 }
 
-java_defaults {
-    name: "loadlibrarytest_app_defaults",
-    defaults: ["art_module_source_build_java_defaults"],
-
-    // TODO(mast): Use old target SDK to avoid filtering on uses_library lists.
-    // Figure out what we need to do to make <uses-native-library> work in the
-    // test apps so we can use that instead.
-    sdk_version: "30",
-
+java_library {
+    name: "loadlibrarytest_test_utils",
     static_libs: [
         "androidx.test.ext.junit",
         "androidx.test.ext.truth",
+    ],
+    srcs: ["src/android/test/lib/TestUtils.java"],
+}
+
+// Test fixture that represents a shared library in /system/framework.
+java_library {
+    name: "libnativeloader_system_shared_lib",
+    installable: true,
+    srcs: ["src/android/test/systemsharedlib/SystemSharedLib.java"],
+}
+
+// Test fixture that represents a shared library in /system_ext/framework.
+java_library {
+    name: "libnativeloader_system_ext_shared_lib",
+    installable: true,
+    srcs: ["src/android/test/systemextsharedlib/SystemExtSharedLib.java"],
+}
+
+java_defaults {
+    name: "loadlibrarytest_app_defaults",
+    defaults: ["art_module_source_build_java_defaults"],
+    static_libs: [
+        "androidx.test.ext.junit",
         "androidx.test.rules",
+        "loadlibrarytest_test_utils",
+    ],
+    libs: [
+        "libnativeloader_system_shared_lib",
+        "libnativeloader_system_ext_shared_lib",
     ],
 }
 
@@ -74,6 +94,7 @@
 android_test_helper_app {
     name: "loadlibrarytest_system_ext_app",
     defaults: ["loadlibrarytest_app_defaults"],
+    system_ext_specific: true,
     manifest: "loadlibrarytest_system_ext_app_manifest.xml",
     // /system_ext should behave the same as /system, so use the same test class there.
     srcs: ["src/android/test/app/SystemAppTest.java"],
@@ -82,6 +103,7 @@
 android_test_helper_app {
     name: "loadlibrarytest_product_app",
     defaults: ["loadlibrarytest_app_defaults"],
+    product_specific: true,
     manifest: "loadlibrarytest_product_app_manifest.xml",
     srcs: ["src/android/test/app/ProductAppTest.java"],
 }
@@ -89,22 +111,37 @@
 android_test_helper_app {
     name: "loadlibrarytest_vendor_app",
     defaults: ["loadlibrarytest_app_defaults"],
+    vendor: true,
     manifest: "loadlibrarytest_vendor_app_manifest.xml",
     srcs: ["src/android/test/app/VendorAppTest.java"],
 }
 
+// A normal app installed in /data.
+android_test_helper_app {
+    name: "loadlibrarytest_data_app",
+    defaults: ["loadlibrarytest_app_defaults"],
+    manifest: "loadlibrarytest_data_app_manifest.xml",
+    srcs: ["src/android/test/app/DataAppTest.java"],
+}
+
 java_test_host {
     name: "libnativeloader_e2e_tests",
     defaults: ["art_module_source_build_java_defaults"],
     srcs: ["src/android/test/hostside/*.java"],
-    libs: ["tradefed"],
-    java_resources: [
+    libs: [
+        "compatibility-tradefed",
+        "tradefed",
+    ],
+    data: [
         ":library_container_app",
+        ":libnativeloader_system_shared_lib",
+        ":libnativeloader_system_ext_shared_lib",
         ":loadlibrarytest_system_priv_app",
         ":loadlibrarytest_system_app",
         ":loadlibrarytest_system_ext_app",
         ":loadlibrarytest_product_app",
         ":loadlibrarytest_vendor_app",
+        ":loadlibrarytest_data_app",
     ],
     test_config: "libnativeloader_e2e_tests.xml",
     test_suites: ["general-tests"],
diff --git a/libnativeloader/test/loadlibrarytest_data_app_manifest.xml b/libnativeloader/test/loadlibrarytest_data_app_manifest.xml
new file mode 100644
index 0000000..2af0af4
--- /dev/null
+++ b/libnativeloader/test/loadlibrarytest_data_app_manifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2022 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.test.app.data">
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.test.app.data" />
+    <application>
+        <uses-library android:name="android.test.systemsharedlib" />
+        <uses-library android:name="android.test.systemextsharedlib" />
+        <uses-native-library android:required="false" android:name="libfoo.oem1.so" />
+        <uses-native-library android:required="false" android:name="libbar.oem1.so" />
+        <uses-native-library android:required="false" android:name="libfoo.oem2.so" />
+        <!--libbar.oem2.so is left out -->
+        <uses-native-library android:required="false" android:name="libfoo.product1.so" />
+        <uses-native-library android:required="false" android:name="libbar.product1.so" />
+    </application>
+</manifest>
+
diff --git a/libnativeloader/test/loadlibrarytest_product_app_manifest.xml b/libnativeloader/test/loadlibrarytest_product_app_manifest.xml
index a791795..614f33f 100644
--- a/libnativeloader/test/loadlibrarytest_product_app_manifest.xml
+++ b/libnativeloader/test/loadlibrarytest_product_app_manifest.xml
@@ -19,5 +19,15 @@
           package="android.test.app.product">
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                      android:targetPackage="android.test.app.product" />
+    <application>
+        <uses-library android:name="android.test.systemsharedlib" />
+        <uses-library android:name="android.test.systemextsharedlib" />
+        <uses-native-library android:required="false" android:name="libfoo.oem1.so" />
+        <uses-native-library android:required="false" android:name="libbar.oem1.so" />
+        <uses-native-library android:required="false" android:name="libfoo.oem2.so" />
+        <!--libbar.oem2.so is left out -->
+        <uses-native-library android:required="false" android:name="libfoo.product1.so" />
+        <uses-native-library android:required="false" android:name="libbar.product1.so" />
+    </application>
 </manifest>
 
diff --git a/libnativeloader/test/loadlibrarytest_system_app_manifest.xml b/libnativeloader/test/loadlibrarytest_system_app_manifest.xml
index 3cbd054..5711f65 100644
--- a/libnativeloader/test/loadlibrarytest_system_app_manifest.xml
+++ b/libnativeloader/test/loadlibrarytest_system_app_manifest.xml
@@ -19,5 +19,13 @@
           package="android.test.app.system">
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                      android:targetPackage="android.test.app.system" />
+    <application>
+        <uses-library android:name="android.test.systemsharedlib" />
+        <uses-library android:name="android.test.systemextsharedlib" />
+        <!-- System apps get a shared classloader namespace, so they don't need
+             uses-native-library entries for anything in /system. -->
+        <uses-native-library android:required="false" android:name="libfoo.product1.so" />
+        <uses-native-library android:required="false" android:name="libbar.product1.so" />
+    </application>
 </manifest>
 
diff --git a/libnativeloader/test/loadlibrarytest_system_ext_app_manifest.xml b/libnativeloader/test/loadlibrarytest_system_ext_app_manifest.xml
index 83ca779..8aa3fa9 100644
--- a/libnativeloader/test/loadlibrarytest_system_ext_app_manifest.xml
+++ b/libnativeloader/test/loadlibrarytest_system_ext_app_manifest.xml
@@ -19,5 +19,13 @@
           package="android.test.app.system_ext">
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                      android:targetPackage="android.test.app.system_ext" />
+    <application>
+        <uses-library android:name="android.test.systemsharedlib" />
+        <uses-library android:name="android.test.systemextsharedlib" />
+        <!-- System apps get a shared classloader namespace, so they don't need
+             uses-native-library entries for anything in /system. -->
+        <uses-native-library android:required="false" android:name="libfoo.product1.so" />
+        <uses-native-library android:required="false" android:name="libbar.product1.so" />
+    </application>
 </manifest>
 
diff --git a/libnativeloader/test/loadlibrarytest_system_priv_app_manifest.xml b/libnativeloader/test/loadlibrarytest_system_priv_app_manifest.xml
index 0ad1aa5..126453c 100644
--- a/libnativeloader/test/loadlibrarytest_system_priv_app_manifest.xml
+++ b/libnativeloader/test/loadlibrarytest_system_priv_app_manifest.xml
@@ -19,5 +19,13 @@
           package="android.test.app.system_priv">
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                      android:targetPackage="android.test.app.system_priv" />
+    <application>
+        <uses-library android:name="android.test.systemsharedlib" />
+        <uses-library android:name="android.test.systemextsharedlib" />
+        <!-- System apps get a shared classloader namespace, so they don't need
+             uses-native-library entries for anything in /system. -->
+        <uses-native-library android:required="false" android:name="libfoo.product1.so" />
+        <uses-native-library android:required="false" android:name="libbar.product1.so" />
+    </application>
 </manifest>
 
diff --git a/libnativeloader/test/loadlibrarytest_vendor_app_manifest.xml b/libnativeloader/test/loadlibrarytest_vendor_app_manifest.xml
index b7073c6..a2a9f64 100644
--- a/libnativeloader/test/loadlibrarytest_vendor_app_manifest.xml
+++ b/libnativeloader/test/loadlibrarytest_vendor_app_manifest.xml
@@ -19,5 +19,15 @@
           package="android.test.app.vendor">
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                      android:targetPackage="android.test.app.vendor" />
+    <application>
+        <uses-library android:name="android.test.systemsharedlib" />
+        <uses-library android:name="android.test.systemextsharedlib" />
+        <uses-native-library android:required="false" android:name="libfoo.oem1.so" />
+        <uses-native-library android:required="false" android:name="libbar.oem1.so" />
+        <uses-native-library android:required="false" android:name="libfoo.oem2.so" />
+        <!--libbar.oem2.so is left out -->
+        <uses-native-library android:required="false" android:name="libfoo.product1.so" />
+        <uses-native-library android:required="false" android:name="libbar.product1.so" />
+    </application>
 </manifest>
 
diff --git a/libnativeloader/test/src/android/test/app/DataAppTest.java b/libnativeloader/test/src/android/test/app/DataAppTest.java
new file mode 100644
index 0000000..767a7b1
--- /dev/null
+++ b/libnativeloader/test/src/android/test/app/DataAppTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 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.test.app;
+
+import android.test.lib.TestUtils;
+import android.test.systemextsharedlib.SystemExtSharedLib;
+import android.test.systemsharedlib.SystemSharedLib;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DataAppTest {
+    @Test
+    public void testLoadExtendedPublicLibraries() {
+        System.loadLibrary("foo.oem1");
+        System.loadLibrary("bar.oem1");
+        System.loadLibrary("foo.oem2");
+        TestUtils.assertLinkerNamespaceError(
+                () -> System.loadLibrary("bar.oem2")); // Missing <uses-native-library>.
+        System.loadLibrary("foo.product1");
+        System.loadLibrary("bar.product1");
+    }
+
+    @Test
+    public void testLoadPrivateLibraries() {
+        TestUtils.assertLinkerNamespaceError(() -> System.loadLibrary("system_private1"));
+        TestUtils.assertLinkerNamespaceError(() -> System.loadLibrary("systemext_private1"));
+        TestUtils.assertLibraryNotFound(() -> System.loadLibrary("product_private1"));
+        TestUtils.assertLibraryNotFound(() -> System.loadLibrary("vendor_private1"));
+    }
+
+    @Test
+    public void testLoadPrivateLibrariesViaSystemSharedLib() {
+        SystemSharedLib.loadLibrary("system_private2");
+        SystemSharedLib.loadLibrary("systemext_private2");
+        TestUtils.assertLibraryNotFound(() -> SystemSharedLib.loadLibrary("product_private2"));
+        TestUtils.assertLibraryNotFound(() -> SystemSharedLib.loadLibrary("vendor_private2"));
+    }
+
+    @Test
+    public void testLoadPrivateLibrariesViaSystemExtSharedLib() {
+        SystemExtSharedLib.loadLibrary("system_private3");
+        SystemExtSharedLib.loadLibrary("systemext_private3");
+        TestUtils.assertLibraryNotFound(() -> SystemExtSharedLib.loadLibrary("product_private3"));
+        TestUtils.assertLibraryNotFound(() -> SystemExtSharedLib.loadLibrary("vendor_private3"));
+    }
+}
diff --git a/libnativeloader/test/src/android/test/app/ProductAppTest.java b/libnativeloader/test/src/android/test/app/ProductAppTest.java
index 243e2c7..1f36798 100644
--- a/libnativeloader/test/src/android/test/app/ProductAppTest.java
+++ b/libnativeloader/test/src/android/test/app/ProductAppTest.java
@@ -16,9 +16,9 @@
 
 package android.test.app;
 
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
+import android.test.lib.TestUtils;
+import android.test.systemextsharedlib.SystemExtSharedLib;
+import android.test.systemsharedlib.SystemSharedLib;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 import org.junit.Test;
@@ -28,12 +28,37 @@
 @RunWith(AndroidJUnit4.class)
 public class ProductAppTest {
     @Test
-    public void testLoadLibraries() {
+    public void testLoadExtendedPublicLibraries() {
         System.loadLibrary("foo.oem1");
         System.loadLibrary("bar.oem1");
         System.loadLibrary("foo.oem2");
-        System.loadLibrary("bar.oem2");
+        TestUtils.assertLinkerNamespaceError(
+                () -> System.loadLibrary("bar.oem2")); // Missing <uses-native-library>.
         System.loadLibrary("foo.product1");
         System.loadLibrary("bar.product1");
     }
+
+    @Test
+    public void testLoadPrivateLibraries() {
+        TestUtils.assertLinkerNamespaceError(() -> System.loadLibrary("system_private1"));
+        TestUtils.assertLinkerNamespaceError(() -> System.loadLibrary("systemext_private1"));
+        System.loadLibrary("product_private1");
+        TestUtils.assertLibraryNotFound(() -> System.loadLibrary("vendor_private1"));
+    }
+
+    @Test
+    public void testLoadPrivateLibrariesViaSystemSharedLib() {
+        SystemSharedLib.loadLibrary("system_private2");
+        SystemSharedLib.loadLibrary("systemext_private2");
+        TestUtils.assertLibraryNotFound(() -> SystemSharedLib.loadLibrary("product_private2"));
+        TestUtils.assertLibraryNotFound(() -> SystemSharedLib.loadLibrary("vendor_private2"));
+    }
+
+    @Test
+    public void testLoadPrivateLibrariesViaSystemExtSharedLib() {
+        SystemExtSharedLib.loadLibrary("system_private3");
+        SystemExtSharedLib.loadLibrary("systemext_private3");
+        TestUtils.assertLibraryNotFound(() -> SystemExtSharedLib.loadLibrary("product_private3"));
+        TestUtils.assertLibraryNotFound(() -> SystemExtSharedLib.loadLibrary("vendor_private3"));
+    }
 }
diff --git a/libnativeloader/test/src/android/test/app/SystemAppTest.java b/libnativeloader/test/src/android/test/app/SystemAppTest.java
index dd680f5..197a40c 100644
--- a/libnativeloader/test/src/android/test/app/SystemAppTest.java
+++ b/libnativeloader/test/src/android/test/app/SystemAppTest.java
@@ -16,20 +16,20 @@
 
 package android.test.app;
 
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
+import android.test.lib.TestUtils;
+import android.test.systemextsharedlib.SystemExtSharedLib;
+import android.test.systemsharedlib.SystemSharedLib;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-// These tests are run in both /system and /system_ext.
+// These tests are run from /system/app, /system/priv-app, and /system_ext/app.
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class SystemAppTest {
     @Test
-    public void testLoadLibraries() {
+    public void testLoadExtendedPublicLibraries() {
         System.loadLibrary("foo.oem1");
         System.loadLibrary("bar.oem1");
         System.loadLibrary("foo.oem2");
@@ -37,4 +37,28 @@
         System.loadLibrary("foo.product1");
         System.loadLibrary("bar.product1");
     }
+
+    @Test
+    public void testLoadPrivateLibraries() {
+        System.loadLibrary("system_private1");
+        System.loadLibrary("systemext_private1");
+        TestUtils.assertLibraryNotFound(() -> System.loadLibrary("product_private1"));
+        TestUtils.assertLibraryNotFound(() -> System.loadLibrary("vendor_private1"));
+    }
+
+    @Test
+    public void testLoadPrivateLibrariesViaSystemSharedLib() {
+        SystemSharedLib.loadLibrary("system_private2");
+        SystemSharedLib.loadLibrary("systemext_private2");
+        TestUtils.assertLibraryNotFound(() -> SystemSharedLib.loadLibrary("product_private2"));
+        TestUtils.assertLibraryNotFound(() -> SystemSharedLib.loadLibrary("vendor_private2"));
+    }
+
+    @Test
+    public void testLoadPrivateLibrariesViaSystemExtSharedLib() {
+        SystemExtSharedLib.loadLibrary("system_private3");
+        SystemExtSharedLib.loadLibrary("systemext_private3");
+        TestUtils.assertLibraryNotFound(() -> SystemExtSharedLib.loadLibrary("product_private3"));
+        TestUtils.assertLibraryNotFound(() -> SystemExtSharedLib.loadLibrary("vendor_private3"));
+    }
 }
diff --git a/libnativeloader/test/src/android/test/app/VendorAppTest.java b/libnativeloader/test/src/android/test/app/VendorAppTest.java
index a830d06..c9ce8db 100644
--- a/libnativeloader/test/src/android/test/app/VendorAppTest.java
+++ b/libnativeloader/test/src/android/test/app/VendorAppTest.java
@@ -16,9 +16,9 @@
 
 package android.test.app;
 
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
+import android.test.lib.TestUtils;
+import android.test.systemextsharedlib.SystemExtSharedLib;
+import android.test.systemsharedlib.SystemSharedLib;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 import org.junit.Test;
@@ -28,19 +28,40 @@
 @RunWith(AndroidJUnit4.class)
 public class VendorAppTest {
     @Test
-    public void testLoadLibraries() {
-        assertLinkerNamespaceError("foo.oem1");
-        assertLinkerNamespaceError("bar.oem1");
-        assertLinkerNamespaceError("foo.oem2");
-        assertLinkerNamespaceError("bar.oem2");
+    public void testLoadExtendedPublicLibraries() {
+        TestUtils.assertLinkerNamespaceError(() -> System.loadLibrary("foo.oem1"));
+        TestUtils.assertLinkerNamespaceError(() -> System.loadLibrary("bar.oem1"));
+        TestUtils.assertLinkerNamespaceError(() -> System.loadLibrary("foo.oem2"));
+        TestUtils.assertLinkerNamespaceError(() -> System.loadLibrary("bar.oem2"));
         System.loadLibrary("foo.product1");
         System.loadLibrary("bar.product1");
     }
 
-    private void assertLinkerNamespaceError(String libraryName) {
-        Throwable t =
-                assertThrows(UnsatisfiedLinkError.class, () -> System.loadLibrary(libraryName));
-        assertThat(t.getMessage())
-                .containsMatch("dlopen failed: .* is not accessible for the namespace");
+    @Test
+    public void testLoadPrivateLibraries() {
+        TestUtils.assertLinkerNamespaceError(() -> System.loadLibrary("system_private1"));
+        TestUtils.assertLinkerNamespaceError(() -> System.loadLibrary("systemext_private1"));
+        TestUtils.assertLibraryNotFound(() -> System.loadLibrary("product_private1"));
+        // TODO(mast): The vendor app fails to load a private vendor library because it gets
+        // classified as untrusted_app in SELinux, which doesn't have access to vendor_file. Even an
+        // app in /vendor/priv-app, which gets classified as priv_app, still doesn't have access to
+        // vendor_file. Check that the test setup is correct and if this is WAI.
+        TestUtils.assertLibraryNotFound(() -> System.loadLibrary("vendor_private1"));
+    }
+
+    @Test
+    public void testLoadPrivateLibrariesViaSystemSharedLib() {
+        SystemSharedLib.loadLibrary("system_private2");
+        SystemSharedLib.loadLibrary("systemext_private2");
+        TestUtils.assertLibraryNotFound(() -> SystemSharedLib.loadLibrary("product_private2"));
+        TestUtils.assertLibraryNotFound(() -> SystemSharedLib.loadLibrary("vendor_private2"));
+    }
+
+    @Test
+    public void testLoadPrivateLibrariesViaSystemExtSharedLib() {
+        SystemExtSharedLib.loadLibrary("system_private3");
+        SystemExtSharedLib.loadLibrary("systemext_private3");
+        TestUtils.assertLibraryNotFound(() -> SystemExtSharedLib.loadLibrary("product_private3"));
+        TestUtils.assertLibraryNotFound(() -> SystemExtSharedLib.loadLibrary("vendor_private3"));
     }
 }
diff --git a/libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java b/libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java
index 7e2d9de..c929037 100644
--- a/libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java
+++ b/libnativeloader/test/src/android/test/hostside/LibnativeloaderTest.java
@@ -19,6 +19,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
@@ -54,19 +56,28 @@
 
     @BeforeClassWithInfo
     public static void beforeClassWithDevice(TestInformation testInfo) throws Exception {
-        DeviceContext ctx = new DeviceContext(testInfo.getContext(), testInfo.getDevice());
+        DeviceContext ctx = new DeviceContext(
+                testInfo.getContext(), testInfo.getDevice(), testInfo.getBuildInfo());
 
         // A soft reboot is slow, so do setup for all tests and reboot once.
 
         ctx.mDevice.remountSystemWritable();
-        try (ZipFile libApk = openLibContainerApk()) {
-            ctx.pushSystemOemLibs(libApk);
-            ctx.pushProductLibs(libApk);
-        }
 
-        // "Install" apps in various partitions through plain adb push. We need them in these
-        // locations to test library loading restrictions, so we cannot use
-        // ITestDevice.installPackage for it since it only installs in /data.
+        File libContainerApk = ctx.mBuildHelper.getTestFile("library_container_app.apk");
+        try (ZipFile libApk = new ZipFile(libContainerApk)) {
+            ctx.pushExtendedPublicSystemOemLibs(libApk);
+            ctx.pushExtendedPublicProductLibs(libApk);
+            ctx.pushPrivateLibs(libApk);
+        }
+        ctx.pushSystemSharedLib("/system/framework", "android.test.systemsharedlib",
+                "libnativeloader_system_shared_lib.jar");
+        ctx.pushSystemSharedLib("/system_ext/framework", "android.test.systemextsharedlib",
+                "libnativeloader_system_ext_shared_lib.jar");
+
+        // "Install" apps in various partitions through plain adb push followed by a soft reboot. We
+        // need them in these locations to test library loading restrictions, so for all except
+        // loadlibrarytest_data_app we cannot use ITestDevice.installPackage for it since it only
+        // installs in /data.
 
         // For testSystemPrivApp
         ctx.pushApk("loadlibrarytest_system_priv_app", "/system/priv-app");
@@ -85,13 +96,21 @@
 
         ctx.softReboot();
 
+        // For testDataApp. Install this the normal way after the system server restart.
+        ctx.installPackage("loadlibrarytest_data_app");
+
         testInfo.properties().put(CLEANUP_PATHS_KEY, ctx.mCleanup.getPathList());
     }
 
     @AfterClassWithInfo
     public static void afterClassWithDevice(TestInformation testInfo) throws Exception {
+        ITestDevice device = testInfo.getDevice();
+
+        // Uninstall loadlibrarytest_data_app.
+        device.uninstallPackage("android.test.app.data");
+
         String cleanupPathList = testInfo.properties().get(CLEANUP_PATHS_KEY);
-        CleanupPaths cleanup = new CleanupPaths(testInfo.getDevice(), cleanupPathList);
+        CleanupPaths cleanup = new CleanupPaths(device, cleanupPathList);
         cleanup.cleanup();
     }
 
@@ -123,6 +142,11 @@
         runDeviceTests("android.test.app.vendor", "android.test.app.VendorAppTest");
     }
 
+    @Test
+    public void testDataApp() throws Exception {
+        runDeviceTests("android.test.app.data", "android.test.app.DataAppTest");
+    }
+
     // Utility class that keeps track of a set of paths the need to be deleted after testing.
     private static class CleanupPaths {
         private ITestDevice mDevice;
@@ -171,18 +195,20 @@
     private static class DeviceContext implements AutoCloseable {
         IInvocationContext mContext;
         ITestDevice mDevice;
+        CompatibilityBuildHelper mBuildHelper;
         CleanupPaths mCleanup;
         private String mTestArch;
 
-        DeviceContext(IInvocationContext context, ITestDevice device) {
+        DeviceContext(IInvocationContext context, ITestDevice device, IBuildInfo buildInfo) {
             mContext = context;
             mDevice = device;
+            mBuildHelper = new CompatibilityBuildHelper(buildInfo);
             mCleanup = new CleanupPaths(mDevice);
         }
 
         public void close() throws DeviceNotAvailableException { mCleanup.cleanup(); }
 
-        void pushSystemOemLibs(ZipFile libApk) throws Exception {
+        void pushExtendedPublicSystemOemLibs(ZipFile libApk) throws Exception {
             pushNativeTestLib(libApk, "/system/${LIB}/libfoo.oem1.so");
             pushNativeTestLib(libApk, "/system/${LIB}/libbar.oem1.so");
             pushString("libfoo.oem1.so\n"
@@ -196,7 +222,7 @@
                     "/system/etc/public.libraries-oem2.txt");
         }
 
-        void pushProductLibs(ZipFile libApk) throws Exception {
+        void pushExtendedPublicProductLibs(ZipFile libApk) throws Exception {
             pushNativeTestLib(libApk, "/product/${LIB}/libfoo.product1.so");
             pushNativeTestLib(libApk, "/product/${LIB}/libbar.product1.so");
             pushString("libfoo.product1.so\n"
@@ -204,6 +230,27 @@
                     "/product/etc/public.libraries-product1.txt");
         }
 
+        void pushPrivateLibs(ZipFile libApk) throws Exception {
+            // Push the libraries once for each test. Since we cannot unload them, we need a fresh
+            // never-before-loaded library in each loadLibrary call.
+            for (int i = 1; i <= 3; ++i) {
+                pushNativeTestLib(libApk, "/system/${LIB}/libsystem_private" + i + ".so");
+                pushNativeTestLib(libApk, "/system_ext/${LIB}/libsystemext_private" + i + ".so");
+                pushNativeTestLib(libApk, "/product/${LIB}/libproduct_private" + i + ".so");
+                pushNativeTestLib(libApk, "/vendor/${LIB}/libvendor_private" + i + ".so");
+            }
+        }
+
+        void pushSystemSharedLib(String packageDir, String packageName, String buildJarName)
+                throws Exception {
+            String path = packageDir + "/" + packageName + ".jar";
+            pushFile(buildJarName, path);
+            pushString("<permissions>\n"
+                            + "<library name=\"" + packageName + "\" file=\"" + path + "\" />\n"
+                            + "</permissions>\n",
+                    "system/etc/permissions/" + packageName + ".xml");
+        }
+
         void softReboot() throws DeviceNotAvailableException {
             assertCommandSucceeds("setprop dev.bootcomplete 0");
             assertCommandSucceeds("stop");
@@ -228,15 +275,14 @@
             assertThat(mDevice.pushString(fileContents, destPath)).isTrue();
         }
 
-        // Like pushString, but extracts a Java resource and pushes that.
-        void pushResource(String resourceName, String destPath) throws Exception {
-            File hostTempFile = extractResourceToTempFile(resourceName);
+        // Like pushString, but pushes a data file included in the host test.
+        void pushFile(String fileName, String destPath) throws Exception {
             mCleanup.addPath(destPath);
-            assertThat(mDevice.pushFile(hostTempFile, destPath)).isTrue();
+            assertThat(mDevice.pushFile(mBuildHelper.getTestFile(fileName), destPath)).isTrue();
         }
 
         void pushApk(String apkBaseName, String destPath) throws Exception {
-            pushResource("/" + apkBaseName + ".apk",
+            pushFile(apkBaseName + ".apk",
                     destPath + "/" + apkBaseName + "/" + apkBaseName + ".apk");
         }
 
@@ -261,6 +307,12 @@
             assertThat(mDevice.pushFile(libraryTempFile, destPath)).isTrue();
         }
 
+        void installPackage(String apkBaseName) throws Exception {
+            assertThat(mDevice.installPackage(mBuildHelper.getTestFile(apkBaseName + ".apk"),
+                               false /* reinstall */))
+                    .isNull();
+        }
+
         String assertCommandSucceeds(String command) throws DeviceNotAvailableException {
             CommandResult result = mDevice.executeShellV2Command(command);
             assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0);
@@ -269,20 +321,6 @@
         }
     }
 
-    static private ZipFile openLibContainerApk() throws Exception {
-        return new ZipFile(extractResourceToTempFile("/library_container_app.apk"));
-    }
-
-    static private File extractResourceToTempFile(String resourceName) throws Exception {
-        assertThat(resourceName).startsWith("/");
-        try (InputStream inStream = LibnativeloaderTest.class.getResourceAsStream(resourceName)) {
-            assertWithMessage("Failed to extract resource " + resourceName)
-                    .that(inStream)
-                    .isNotNull();
-            return writeStreamToTempFile(resourceName.substring(1), inStream);
-        }
-    }
-
     static private File writeStreamToTempFile(String tempFileBaseName, InputStream inStream)
             throws Exception {
         File hostTempFile = File.createTempFile(tempFileBaseName, null);
diff --git a/libnativeloader/test/src/android/test/lib/TestUtils.java b/libnativeloader/test/src/android/test/lib/TestUtils.java
new file mode 100644
index 0000000..eda9993
--- /dev/null
+++ b/libnativeloader/test/src/android/test/lib/TestUtils.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 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.test.lib;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import org.junit.function.ThrowingRunnable;
+
+public final class TestUtils {
+    public static void assertLibraryNotFound(ThrowingRunnable loadLibrary) {
+        Throwable t = assertThrows(UnsatisfiedLinkError.class, loadLibrary);
+        assertThat(t.getMessage()).containsMatch("dlopen failed: library .* not found");
+    }
+
+    public static void assertLinkerNamespaceError(ThrowingRunnable loadLibrary) {
+        Throwable t = assertThrows(UnsatisfiedLinkError.class, loadLibrary);
+        assertThat(t.getMessage())
+                .containsMatch("dlopen failed: .* is not accessible for the namespace");
+    }
+}
diff --git a/libnativeloader/test/src/android/test/systemextsharedlib/SystemExtSharedLib.java b/libnativeloader/test/src/android/test/systemextsharedlib/SystemExtSharedLib.java
new file mode 100644
index 0000000..1240e12
--- /dev/null
+++ b/libnativeloader/test/src/android/test/systemextsharedlib/SystemExtSharedLib.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 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.test.systemextsharedlib;
+
+public final class SystemExtSharedLib {
+    public static void loadLibrary(String name) { System.loadLibrary(name); }
+}
diff --git a/libnativeloader/test/src/android/test/systemsharedlib/SystemSharedLib.java b/libnativeloader/test/src/android/test/systemsharedlib/SystemSharedLib.java
new file mode 100644
index 0000000..8e2af9f
--- /dev/null
+++ b/libnativeloader/test/src/android/test/systemsharedlib/SystemSharedLib.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 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.test.systemsharedlib;
+
+public final class SystemSharedLib {
+    public static void loadLibrary(String name) { System.loadLibrary(name); }
+}
diff --git a/odrefresh/odr_metrics.cc b/odrefresh/odr_metrics.cc
index 4bddb17..a2ce51f 100644
--- a/odrefresh/odr_metrics.cc
+++ b/odrefresh/odr_metrics.cc
@@ -64,16 +64,16 @@
   }
 }
 
-void OdrMetrics::SetCompilationTime(int32_t seconds) {
+void OdrMetrics::SetCompilationTime(int32_t millis) {
   switch (stage_) {
     case Stage::kPrimaryBootClasspath:
-      primary_bcp_compilation_seconds_ = seconds;
+      primary_bcp_compilation_millis_ = millis;
       break;
     case Stage::kSecondaryBootClasspath:
-      secondary_bcp_compilation_seconds_ = seconds;
+      secondary_bcp_compilation_millis_ = millis;
       break;
     case Stage::kSystemServerClasspath:
-      system_server_compilation_seconds_ = seconds;
+      system_server_compilation_millis_ = millis;
       break;
     case Stage::kCheck:
     case Stage::kComplete:
@@ -119,28 +119,31 @@
   if (!trigger_.has_value()) {
     return false;
   }
+  record->odrefresh_metrics_version = kOdrefreshMetricsVersion;
   record->art_apex_version = art_apex_version_;
   record->trigger = static_cast<uint32_t>(trigger_.value());
   record->stage_reached = static_cast<uint32_t>(stage_);
   record->status = static_cast<uint32_t>(status_);
-  record->primary_bcp_compilation_seconds = primary_bcp_compilation_seconds_;
-  record->secondary_bcp_compilation_seconds = secondary_bcp_compilation_seconds_;
-  record->system_server_compilation_seconds = system_server_compilation_seconds_;
   record->cache_space_free_start_mib = cache_space_free_start_mib_;
   record->cache_space_free_end_mib = cache_space_free_end_mib_;
+  record->primary_bcp_compilation_millis = primary_bcp_compilation_millis_;
+  record->secondary_bcp_compilation_millis = secondary_bcp_compilation_millis_;
+  record->system_server_compilation_millis = system_server_compilation_millis_;
   return true;
 }
 
 void OdrMetrics::WriteToFile(const std::string& path, const OdrMetrics* metrics) {
-  OdrMetricsRecord record;
+  OdrMetricsRecord record{};
   if (!metrics->ToRecord(&record)) {
     LOG(ERROR) << "Attempting to report metrics without a compilation trigger.";
     return;
   }
 
-  // Preserve order from frameworks/proto_logging/stats/atoms.proto in metrics file written.
-  std::ofstream ofs(path);
-  ofs << record;
+  const android::base::Result<void>& result = record.WriteToFile(path);
+  if (!result.ok()) {
+    LOG(ERROR) << "Failed to report metrics to file: " << path
+               << ", error: " << result.error().message();
+  }
 }
 
 }  // namespace odrefresh
diff --git a/odrefresh/odr_metrics.h b/odrefresh/odr_metrics.h
index cd80bef..7ebc148 100644
--- a/odrefresh/odr_metrics.h
+++ b/odrefresh/odr_metrics.h
@@ -126,7 +126,7 @@
   static int32_t GetFreeSpaceMiB(const std::string& path);
   static void WriteToFile(const std::string& path, const OdrMetrics* metrics);
 
-  void SetCompilationTime(int32_t seconds);
+  void SetCompilationTime(int32_t millis);
 
   const std::string cache_directory_;
   const std::string metrics_file_;
@@ -137,11 +137,11 @@
   Stage stage_ = Stage::kUnknown;
   Status status_ = Status::kUnknown;
 
-  int32_t primary_bcp_compilation_seconds_ = 0;
-  int32_t secondary_bcp_compilation_seconds_ = 0;
-  int32_t system_server_compilation_seconds_ = 0;
   int32_t cache_space_free_start_mib_ = 0;
   int32_t cache_space_free_end_mib_ = 0;
+  int32_t primary_bcp_compilation_millis_ = 0;
+  int32_t secondary_bcp_compilation_millis_ = 0;
+  int32_t system_server_compilation_millis_ = 0;
 
   friend class ScopedOdrCompilationTimer;
 };
@@ -155,8 +155,8 @@
 
   ~ScopedOdrCompilationTimer() {
     auto elapsed_time = std::chrono::steady_clock::now() - start_;
-    auto elapsed_seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed_time);
-    metrics_.SetCompilationTime(static_cast<int32_t>(elapsed_seconds.count()));
+    auto elapsed_millis = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed_time);
+    metrics_.SetCompilationTime(static_cast<int32_t>(elapsed_millis.count()));
   }
 
  private:
diff --git a/odrefresh/odr_metrics_record.cc b/odrefresh/odr_metrics_record.cc
index fc135d3..b8c2f80 100644
--- a/odrefresh/odr_metrics_record.cc
+++ b/odrefresh/odr_metrics_record.cc
@@ -14,59 +14,110 @@
  * limitations under the License.
  */
 
+#include "android-base/logging.h"
 #include "odr_metrics_record.h"
+#include "tinyxml2.h"
 
 #include <iosfwd>
-#include <istream>
-#include <ostream>
-#include <streambuf>
 #include <string>
 
 namespace art {
 namespace odrefresh {
 
-std::istream& operator>>(std::istream& is, OdrMetricsRecord& record) {
-  // Block I/O related exceptions
-  auto saved_exceptions = is.exceptions();
-  is.exceptions(std::ios_base::iostate {});
+namespace {
+android::base::Result<int64_t> ReadInt64(tinyxml2::XMLElement* parent, const char* name) {
+  tinyxml2::XMLElement* element = parent->FirstChildElement(name);
+  if (element == nullptr) {
+    return Errorf("Expected Odrefresh metric {} not found", name);
+  }
 
-  // The order here matches the field order of MetricsRecord.
-  is >> record.art_apex_version >> std::ws;
-  is >> record.trigger >> std::ws;
-  is >> record.stage_reached >> std::ws;
-  is >> record.status >> std::ws;
-  is >> record.primary_bcp_compilation_seconds >> std::ws;
-  is >> record.secondary_bcp_compilation_seconds >> std::ws;
-  is >> record.system_server_compilation_seconds >> std::ws;
-  is >> record.cache_space_free_start_mib >> std::ws;
-  is >> record.cache_space_free_end_mib >> std::ws;
-
-  // Restore I/O related exceptions
-  is.exceptions(saved_exceptions);
-  return is;
+  int64_t metric;
+  tinyxml2::XMLError result = element->QueryInt64Text(&metric);
+  if (result == tinyxml2::XML_SUCCESS) {
+    return metric;
+  } else {
+    return Errorf("Odrefresh metric {} is not an int64", name);
+  }
 }
 
-std::ostream& operator<<(std::ostream& os, const OdrMetricsRecord& record) {
-  static const char kSpace = ' ';
+android::base::Result<int32_t> ReadInt32(tinyxml2::XMLElement* parent, const char* name) {
+  tinyxml2::XMLElement* element = parent->FirstChildElement(name);
+  if (element == nullptr) {
+    return Errorf("Expected Odrefresh metric {} not found", name);
+  }
 
-  // Block I/O related exceptions
-  auto saved_exceptions = os.exceptions();
-  os.exceptions(std::ios_base::iostate {});
+  int32_t metric;
+  tinyxml2::XMLError result = element->QueryIntText(&metric);
+  if (result == tinyxml2::XML_SUCCESS) {
+    return metric;
+  } else {
+    return Errorf("Odrefresh metric {} is not an int32", name);
+  }
+}
+
+template <typename T>
+void AddMetric(tinyxml2::XMLElement* parent, const char* name, const T& value) {
+  parent->InsertNewChildElement(name)->SetText(value);
+}
+}  // namespace
+
+android::base::Result<void> OdrMetricsRecord::ReadFromFile(const std::string& filename) {
+  tinyxml2::XMLDocument xml_document;
+  tinyxml2::XMLError result = xml_document.LoadFile(filename.data());
+  if (result != tinyxml2::XML_SUCCESS) {
+    return android::base::Error() << xml_document.ErrorStr();
+  }
+
+  tinyxml2::XMLElement* metrics = xml_document.FirstChildElement("odrefresh_metrics");
+  if (metrics == nullptr) {
+    return Errorf("odrefresh_metrics element not found in {}", filename);
+  }
+
+  odrefresh_metrics_version = OR_RETURN(ReadInt32(metrics, "odrefresh_metrics_version"));
+  if (odrefresh_metrics_version != kOdrefreshMetricsVersion) {
+    return Errorf("odrefresh_metrics_version {} is different than expected ({})",
+                  odrefresh_metrics_version,
+                  kOdrefreshMetricsVersion);
+  }
+
+  art_apex_version = OR_RETURN(ReadInt64(metrics, "art_apex_version"));
+  trigger = OR_RETURN(ReadInt32(metrics, "trigger"));
+  stage_reached = OR_RETURN(ReadInt32(metrics, "stage_reached"));
+  status = OR_RETURN(ReadInt32(metrics, "status"));
+  cache_space_free_start_mib = OR_RETURN(ReadInt32(metrics, "cache_space_free_start_mib"));
+  cache_space_free_end_mib = OR_RETURN(ReadInt32(metrics, "cache_space_free_end_mib"));
+  primary_bcp_compilation_millis = OR_RETURN(
+      ReadInt32(metrics, "primary_bcp_compilation_millis"));
+  secondary_bcp_compilation_millis = OR_RETURN(
+      ReadInt32(metrics, "secondary_bcp_compilation_millis"));
+  system_server_compilation_millis = OR_RETURN(
+      ReadInt32(metrics, "system_server_compilation_millis"));
+  return {};
+}
+
+android::base::Result<void> OdrMetricsRecord::WriteToFile(const std::string& filename) const {
+  tinyxml2::XMLDocument xml_document;
+  tinyxml2::XMLElement* metrics = xml_document.NewElement("odrefresh_metrics");
+  xml_document.InsertEndChild(metrics);
 
   // The order here matches the field order of MetricsRecord.
-  os << record.art_apex_version << kSpace;
-  os << record.trigger << kSpace;
-  os << record.stage_reached << kSpace;
-  os << record.status << kSpace;
-  os << record.primary_bcp_compilation_seconds << kSpace;
-  os << record.secondary_bcp_compilation_seconds << kSpace;
-  os << record.system_server_compilation_seconds << kSpace;
-  os << record.cache_space_free_start_mib << kSpace;
-  os << record.cache_space_free_end_mib << std::endl;
+  AddMetric(metrics, "odrefresh_metrics_version", odrefresh_metrics_version);
+  AddMetric(metrics, "art_apex_version", art_apex_version);
+  AddMetric(metrics, "trigger", trigger);
+  AddMetric(metrics, "stage_reached", stage_reached);
+  AddMetric(metrics, "status", status);
+  AddMetric(metrics, "cache_space_free_start_mib", cache_space_free_start_mib);
+  AddMetric(metrics, "cache_space_free_end_mib", cache_space_free_end_mib);
+  AddMetric(metrics, "primary_bcp_compilation_millis", primary_bcp_compilation_millis);
+  AddMetric(metrics, "secondary_bcp_compilation_millis", secondary_bcp_compilation_millis);
+  AddMetric(metrics, "system_server_compilation_millis", system_server_compilation_millis);
 
-  // Restore I/O related exceptions
-  os.exceptions(saved_exceptions);
-  return os;
+  tinyxml2::XMLError result = xml_document.SaveFile(filename.data(), /*compact=*/true);
+  if (result == tinyxml2::XML_SUCCESS) {
+    return {};
+  } else {
+    return android::base::Error() << xml_document.ErrorStr();
+  }
 }
 
 }  // namespace odrefresh
diff --git a/odrefresh/odr_metrics_record.h b/odrefresh/odr_metrics_record.h
index 9dd51a6..fa95337 100644
--- a/odrefresh/odr_metrics_record.h
+++ b/odrefresh/odr_metrics_record.h
@@ -20,43 +20,42 @@
 #include <cstdint>
 #include <iosfwd>  // For forward-declaration of std::string.
 
+#include "android-base/result.h"
+#include "tinyxml2.h"
+
 namespace art {
 namespace odrefresh {
 
 // Default location for storing metrics from odrefresh.
-constexpr const char* kOdrefreshMetricsFile = "/data/misc/odrefresh/odrefresh-metrics.txt";
+constexpr const char* kOdrefreshMetricsFile = "/data/misc/odrefresh/odrefresh-metrics.xml";
+
+// Initial OdrefreshMetrics version
+static constexpr int32_t kOdrefreshMetricsVersion = 2;
 
 // MetricsRecord is a simpler container for Odrefresh metric values reported to statsd. The order
 // and types of fields here mirror definition of `OdrefreshReported` in
 // frameworks/proto_logging/stats/atoms.proto.
 struct OdrMetricsRecord {
+  int32_t odrefresh_metrics_version;
   int64_t art_apex_version;
   int32_t trigger;
   int32_t stage_reached;
   int32_t status;
-  int32_t primary_bcp_compilation_seconds;
-  int32_t secondary_bcp_compilation_seconds;
-  int32_t system_server_compilation_seconds;
   int32_t cache_space_free_start_mib;
   int32_t cache_space_free_end_mib;
+  int32_t primary_bcp_compilation_millis;
+  int32_t secondary_bcp_compilation_millis;
+  int32_t system_server_compilation_millis;
+
+  // Reads a `MetricsRecord` from an XML file.
+  // Returns an error if the XML document was not found or parsed correctly.
+  android::base::Result<void> ReadFromFile(const std::string& filename);
+
+  // Writes a `MetricsRecord` to an XML file.
+  // Returns an error if the XML document was not saved correctly.
+  android::base::Result<void> WriteToFile(const std::string& filename) const;
 };
 
-// Read a `MetricsRecord` from an `istream`.
-//
-// This method blocks istream related exceptions, the caller should check `is.fail()` is false after
-// calling.
-//
-// Returns `is`.
-std::istream& operator>>(std::istream& is, OdrMetricsRecord& record);
-
-// Write a `MetricsRecord` to an `ostream`.
-//
-// This method blocks ostream related exceptions, the caller should check `os.fail()` is false after
-// calling.
-//
-// Returns `os`
-std::ostream& operator<<(std::ostream& os, const OdrMetricsRecord& record);
-
 }  // namespace odrefresh
 }  // namespace art
 
diff --git a/odrefresh/odr_metrics_record_test.cc b/odrefresh/odr_metrics_record_test.cc
index dd739d6..fbb5b99 100644
--- a/odrefresh/odr_metrics_record_test.cc
+++ b/odrefresh/odr_metrics_record_test.cc
@@ -20,6 +20,8 @@
 
 #include <fstream>
 
+#include "android-base/result-gmock.h"
+#include "android-base/stringprintf.h"
 #include "base/common_art_test.h"
 
 namespace art {
@@ -27,86 +29,124 @@
 
 class OdrMetricsRecordTest : public CommonArtTest {};
 
+using android::base::testing::Ok;
+using android::base::testing::HasError;
+using android::base::testing::WithMessage;
+
 TEST_F(OdrMetricsRecordTest, HappyPath) {
-  const OdrMetricsRecord expected {
+  const OdrMetricsRecord expected{
+    .odrefresh_metrics_version = art::odrefresh::kOdrefreshMetricsVersion,
     .art_apex_version = 0x01233456'789abcde,
     .trigger = 0x01020304,
     .stage_reached = 0x11121314,
     .status = 0x21222324,
-    .primary_bcp_compilation_seconds = 0x31323334,
-    .secondary_bcp_compilation_seconds = 0x41424344,
-    .system_server_compilation_seconds = 0x51525354,
     .cache_space_free_start_mib = 0x61626364,
-    .cache_space_free_end_mib = 0x71727374
+    .cache_space_free_end_mib = 0x71727374,
+    .primary_bcp_compilation_millis = 0x31323334,
+    .secondary_bcp_compilation_millis = 0x41424344,
+    .system_server_compilation_millis = 0x51525354
   };
 
   ScratchDir dir(/*keep_files=*/false);
-  std::string file_path = dir.GetPath() + "/metrics-record.txt";
-
-  {
-    std::ofstream ofs(file_path);
-    ofs << expected;
-    ASSERT_FALSE(ofs.fail());
-    ofs.close();
-  }
+  std::string file_path = dir.GetPath() + "/metrics-record.xml";
+  ASSERT_THAT(expected.WriteToFile(file_path), Ok());
 
   OdrMetricsRecord actual {};
-  {
-    std::ifstream ifs(file_path);
-    ifs >> actual;
-    ASSERT_TRUE(ifs.eof());
-  }
+  ASSERT_THAT(actual.ReadFromFile(file_path), Ok());
 
+  ASSERT_EQ(expected.odrefresh_metrics_version, actual.odrefresh_metrics_version);
   ASSERT_EQ(expected.art_apex_version, actual.art_apex_version);
   ASSERT_EQ(expected.trigger, actual.trigger);
   ASSERT_EQ(expected.stage_reached, actual.stage_reached);
   ASSERT_EQ(expected.status, actual.status);
-  ASSERT_EQ(expected.primary_bcp_compilation_seconds, actual.primary_bcp_compilation_seconds);
-  ASSERT_EQ(expected.secondary_bcp_compilation_seconds, actual.secondary_bcp_compilation_seconds);
-  ASSERT_EQ(expected.system_server_compilation_seconds, actual.system_server_compilation_seconds);
   ASSERT_EQ(expected.cache_space_free_start_mib, actual.cache_space_free_start_mib);
   ASSERT_EQ(expected.cache_space_free_end_mib, actual.cache_space_free_end_mib);
+  ASSERT_EQ(expected.primary_bcp_compilation_millis, actual.primary_bcp_compilation_millis);
+  ASSERT_EQ(expected.secondary_bcp_compilation_millis, actual.secondary_bcp_compilation_millis);
+  ASSERT_EQ(expected.system_server_compilation_millis, actual.system_server_compilation_millis);
   ASSERT_EQ(0, memcmp(&expected, &actual, sizeof(expected)));
 }
 
 TEST_F(OdrMetricsRecordTest, EmptyInput) {
   ScratchDir dir(/*keep_files=*/false);
-  std::string file_path = dir.GetPath() + "/metrics-record.txt";
+  std::string file_path = dir.GetPath() + "/metrics-record.xml";
 
-  std::ifstream ifs(file_path);
-  OdrMetricsRecord record;
-  ifs >> record;
-
-  ASSERT_TRUE(ifs.fail());
-  ASSERT_TRUE(!ifs);
+  OdrMetricsRecord record{};
+  ASSERT_THAT(record.ReadFromFile(file_path), testing::Not(Ok()));
 }
 
-TEST_F(OdrMetricsRecordTest, ClosedInput) {
+TEST_F(OdrMetricsRecordTest, UnexpectedInput) {
   ScratchDir dir(/*keep_files=*/false);
-  std::string file_path = dir.GetPath() + "/metrics-record.txt";
-
-  std::ifstream ifs(file_path);
-  ifs.close();
-
-  OdrMetricsRecord record;
-  ifs >> record;
-
-  ASSERT_TRUE(ifs.fail());
-  ASSERT_TRUE(!ifs);
-}
-
-TEST_F(OdrMetricsRecordTest, ClosedOutput) {
-  ScratchDir dir(/*keep_files=*/false);
-  std::string file_path = dir.GetPath() + "/metrics-record.txt";
+  std::string file_path = dir.GetPath() + "/metrics-record.xml";
 
   std::ofstream ofs(file_path);
+  ofs << "<not_odrefresh_metrics></not_odrefresh_metrics>";
   ofs.close();
 
-  OdrMetricsRecord record {};
-  ofs << record;
+  OdrMetricsRecord record{};
+  ASSERT_THAT(
+      record.ReadFromFile(file_path),
+      HasError(WithMessage("odrefresh_metrics element not found in " + file_path)));
+}
 
-  ASSERT_TRUE(ofs.fail());
-  ASSERT_TRUE(!ofs.good());
+TEST_F(OdrMetricsRecordTest, ExpectedElementNotFound) {
+  ScratchDir dir(/*keep_files=*/false);
+  std::string file_path = dir.GetPath() + "/metrics-record.xml";
+
+  std::ofstream ofs(file_path);
+  ofs << "<odrefresh_metrics>";
+  ofs << "<not_valid_metric>25</not_valid_metric>";
+  ofs << "</odrefresh_metrics>";
+  ofs.close();
+
+  OdrMetricsRecord record{};
+  ASSERT_THAT(
+      record.ReadFromFile(file_path),
+      HasError(WithMessage("Expected Odrefresh metric odrefresh_metrics_version not found")));
+}
+
+TEST_F(OdrMetricsRecordTest, UnexpectedOdrefreshMetricsVersion) {
+  ScratchDir dir(/*keep_files=*/false);
+  std::string file_path = dir.GetPath() + "/metrics-record.xml";
+
+  std::ofstream ofs(file_path);
+  ofs << "<odrefresh_metrics>";
+  ofs << "<odrefresh_metrics_version>0</odrefresh_metrics_version>";
+  ofs << "</odrefresh_metrics>";
+  ofs.close();
+
+  OdrMetricsRecord record{};
+  std::string expected_error = android::base::StringPrintf(
+      "odrefresh_metrics_version 0 is different than expected (%d)",
+      kOdrefreshMetricsVersion);
+  ASSERT_THAT(record.ReadFromFile(file_path),
+              HasError(WithMessage(expected_error)));
+}
+
+TEST_F(OdrMetricsRecordTest, UnexpectedType) {
+  ScratchDir dir(/*keep_files=*/false);
+  std::string file_path = dir.GetPath() + "/metrics-record.xml";
+
+  std::ofstream ofs(file_path);
+  ofs << "<odrefresh_metrics>";
+  ofs << "<odrefresh_metrics_version>" << kOdrefreshMetricsVersion
+      << "</odrefresh_metrics_version>";
+  ofs << "<art_apex_version>81966764218039518</art_apex_version>";
+  ofs << "<trigger>16909060</trigger>";
+  ofs << "<stage_reached>286397204</stage_reached>";
+  ofs << "<status>abcd</status>";  // It should be an int32.
+  ofs << "<cache_space_free_start_mib>1633837924</cache_space_free_start_mib>";
+  ofs << "<cache_space_free_end_mib>1903326068</cache_space_free_end_mib>";
+  ofs << "<primary_bcp_compilation_millis>825373492</primary_bcp_compilation_millis>";
+  ofs << "<secondary_bcp_compilation_millis>1094861636</secondary_bcp_compilation_millis>";
+  ofs << "<system_server_compilation_millis>1364349780</system_server_compilation_millis>";
+  ofs << "</odrefresh_metrics>";
+  ofs.close();
+
+  OdrMetricsRecord record{};
+  ASSERT_THAT(
+      record.ReadFromFile(file_path),
+      HasError(WithMessage("Odrefresh metric status is not an int32")));
 }
 
 }  // namespace odrefresh
diff --git a/odrefresh/odr_metrics_test.cc b/odrefresh/odr_metrics_test.cc
index 4519f00..f222caa 100644
--- a/odrefresh/odr_metrics_test.cc
+++ b/odrefresh/odr_metrics_test.cc
@@ -24,6 +24,7 @@
 #include <memory>
 #include <string>
 
+#include "android-base/result-gmock.h"
 #include "base/common_art_test.h"
 
 namespace art {
@@ -35,7 +36,7 @@
     CommonArtTest::SetUp();
 
     scratch_dir_ = std::make_unique<ScratchDir>();
-    metrics_file_path_ = scratch_dir_->GetPath() + "/metrics.txt";
+    metrics_file_path_ = scratch_dir_->GetPath() + "/metrics.xml";
     cache_directory_ = scratch_dir_->GetPath() + "/dir";
     mkdir(cache_directory_.c_str(), S_IRWXU);
   }
@@ -158,10 +159,10 @@
   EXPECT_TRUE(metrics.ToRecord(&record));
   EXPECT_EQ(OdrMetrics::Stage::kPrimaryBootClasspath,
             enum_cast<OdrMetrics::Stage>(record.stage_reached));
-  EXPECT_NE(0, record.primary_bcp_compilation_seconds);
-  EXPECT_GT(10, record.primary_bcp_compilation_seconds);
-  EXPECT_EQ(0, record.secondary_bcp_compilation_seconds);
-  EXPECT_EQ(0, record.system_server_compilation_seconds);
+  EXPECT_NE(0, record.primary_bcp_compilation_millis);
+  EXPECT_GT(10'000, record.primary_bcp_compilation_millis);
+  EXPECT_EQ(0, record.secondary_bcp_compilation_millis);
+  EXPECT_EQ(0, record.system_server_compilation_millis);
 
   // Secondary boot classpath compilation time.
   {
@@ -172,10 +173,10 @@
   EXPECT_TRUE(metrics.ToRecord(&record));
   EXPECT_EQ(OdrMetrics::Stage::kSecondaryBootClasspath,
             enum_cast<OdrMetrics::Stage>(record.stage_reached));
-  EXPECT_NE(0, record.primary_bcp_compilation_seconds);
-  EXPECT_NE(0, record.secondary_bcp_compilation_seconds);
-  EXPECT_GT(10, record.secondary_bcp_compilation_seconds);
-  EXPECT_EQ(0, record.system_server_compilation_seconds);
+  EXPECT_NE(0, record.primary_bcp_compilation_millis);
+  EXPECT_NE(0, record.secondary_bcp_compilation_millis);
+  EXPECT_GT(10'000, record.secondary_bcp_compilation_millis);
+  EXPECT_EQ(0, record.system_server_compilation_millis);
 
   // system_server classpath compilation time.
   {
@@ -186,10 +187,10 @@
   EXPECT_TRUE(metrics.ToRecord(&record));
   EXPECT_EQ(OdrMetrics::Stage::kSystemServerClasspath,
             enum_cast<OdrMetrics::Stage>(record.stage_reached));
-  EXPECT_NE(0, record.primary_bcp_compilation_seconds);
-  EXPECT_NE(0, record.secondary_bcp_compilation_seconds);
-  EXPECT_NE(0, record.system_server_compilation_seconds);
-  EXPECT_GT(10, record.system_server_compilation_seconds);
+  EXPECT_NE(0, record.primary_bcp_compilation_millis);
+  EXPECT_NE(0, record.secondary_bcp_compilation_millis);
+  EXPECT_NE(0, record.system_server_compilation_millis);
+  EXPECT_GT(10'000, record.system_server_compilation_millis);
 }
 
 TEST_F(OdrMetricsTest, CacheSpaceValuesAreUpdated) {
@@ -207,11 +208,8 @@
     EXPECT_EQ(0, snap.cache_space_free_end_mib);
   }
 
-  OdrMetricsRecord on_disk;
-  std::ifstream ifs(GetMetricsFilePath());
-  EXPECT_TRUE(ifs);
-  ifs >> on_disk;
-  EXPECT_TRUE(ifs);
+  OdrMetricsRecord on_disk{};
+  EXPECT_THAT(on_disk.ReadFromFile(GetMetricsFilePath()), android::base::testing::Ok());
   EXPECT_EQ(snap.cache_space_free_start_mib, on_disk.cache_space_free_start_mib);
   EXPECT_NE(0, on_disk.cache_space_free_end_mib);
 }
diff --git a/odrefresh/odr_statslog_android.cc b/odrefresh/odr_statslog_android.cc
index 7db348e..ee173d4 100644
--- a/odrefresh/odr_statslog_android.cc
+++ b/odrefresh/odr_statslog_android.cc
@@ -103,16 +103,11 @@
 bool ReadValues(const char* metrics_file,
                 /*out*/ OdrMetricsRecord* record,
                 /*out*/ std::string* error_msg) {
-  std::ifstream ifs(metrics_file);
-  if (!ifs) {
-    *error_msg = android::base::StringPrintf(
-        "metrics file '%s' could not be opened: %s", metrics_file, strerror(errno));
-    return false;
-  }
-
-  ifs >> *record;
-  if (!ifs) {
-    *error_msg = "file parsing error.";
+  const android::base::Result<void>& result = record->ReadFromFile(metrics_file);
+  if (!result.ok()) {
+    *error_msg = android::base::StringPrintf("Unable to open or parse metrics file %s (error: %s)",
+                                             metrics_file,
+                                             result.error().message().data());
     return false;
   }
 
@@ -151,16 +146,20 @@
 
   // Write values to statsd. The order of values passed is the same as the order of the
   // fields in OdrMetricsRecord.
-  int bytes_written = art::metrics::statsd::stats_write(metrics::statsd::ODREFRESH_REPORTED,
-                                                        record.art_apex_version,
-                                                        record.trigger,
-                                                        record.stage_reached,
-                                                        record.status,
-                                                        record.primary_bcp_compilation_seconds,
-                                                        record.secondary_bcp_compilation_seconds,
-                                                        record.system_server_compilation_seconds,
-                                                        record.cache_space_free_start_mib,
-                                                        record.cache_space_free_end_mib);
+  int bytes_written = art::metrics::statsd::stats_write(
+      metrics::statsd::ODREFRESH_REPORTED,
+      record.art_apex_version,
+      record.trigger,
+      record.stage_reached,
+      record.status,
+      record.primary_bcp_compilation_millis / 1000,
+      record.secondary_bcp_compilation_millis / 1000,
+      record.system_server_compilation_millis / 1000,
+      record.cache_space_free_start_mib,
+      record.cache_space_free_end_mib,
+      record.primary_bcp_compilation_millis,
+      record.secondary_bcp_compilation_millis,
+      record.system_server_compilation_millis);
   if (bytes_written <= 0) {
     *error_msg = android::base::StringPrintf("stats_write returned %d", bytes_written);
     return false;
diff --git a/test/Android.bp b/test/Android.bp
index 0679bd6..4289c18 100644
--- a/test/Android.bp
+++ b/test/Android.bp
@@ -1121,6 +1121,8 @@
         "2007-virtual-structural-finalizable/src-art/art/Test2007.java",
     ],
     sdk_version: "core_platform",
+    // Make sure that this will be added to the sdk snapshot for S.
+    min_sdk_version: "S",
     // Some ART run-tests contain constructs which break ErrorProne checks;
     // disable `errorprone` builds.
     errorprone: {
@@ -1284,6 +1286,8 @@
         "expected_cts_outputs_gen",
     ],
     sdk_version: "core_current",
+    // Make sure that this will be added to the sdk snapshot for S.
+    min_sdk_version: "S",
 }
 
 art_cc_test {