diff --git a/test/rules/android_binary/r8_integration/BUILD b/test/rules/android_binary/r8_integration/BUILD
index d3f4325ff..dc2ca1947 100644
--- a/test/rules/android_binary/r8_integration/BUILD
+++ b/test/rules/android_binary/r8_integration/BUILD
@@ -24,3 +24,13 @@ build_test(
name = "android_binary_with_neverlink_deps_build_test",
targets = ["//test/rules/android_binary/r8_integration/java/com/neverlink:android_binary_with_neverlink_deps"],
)
+
+py_test(
+ name = "r8_desugaring_test",
+ srcs = ["r8_desugaring_test.py"],
+ args = ["$(location @androidsdk//:dexdump)"],
+ data = [
+ "//test/rules/android_binary/r8_integration/java/com/desugaring:desugaring_app_r8",
+ "@androidsdk//:dexdump",
+ ],
+)
diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/AndroidManifest.xml b/test/rules/android_binary/r8_integration/java/com/desugaring/AndroidManifest.xml
new file mode 100644
index 000000000..67e5eae7c
--- /dev/null
+++ b/test/rules/android_binary/r8_integration/java/com/desugaring/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/BUILD b/test/rules/android_binary/r8_integration/java/com/desugaring/BUILD
new file mode 100644
index 000000000..334093979
--- /dev/null
+++ b/test/rules/android_binary/r8_integration/java/com/desugaring/BUILD
@@ -0,0 +1,16 @@
+load("//rules:rules.bzl", "android_binary", "android_library")
+
+android_library(
+ name = "duration_lib",
+ srcs = ["DurationUser.java"],
+)
+
+android_binary(
+ name = "desugaring_app_r8",
+ srcs = ["DesugaringActivity.java"],
+ manifest = "AndroidManifest.xml",
+ proguard_specs = ["proguard.cfg"],
+ resource_files = glob(["res/**"]),
+ visibility = ["//test/rules/android_binary/r8_integration:__pkg__"],
+ deps = [":duration_lib"],
+)
diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/DesugaringActivity.java b/test/rules/android_binary/r8_integration/java/com/desugaring/DesugaringActivity.java
new file mode 100644
index 000000000..d9c42a681
--- /dev/null
+++ b/test/rules/android_binary/r8_integration/java/com/desugaring/DesugaringActivity.java
@@ -0,0 +1,17 @@
+package com.desugaring;
+
+import android.app.Activity;
+import android.os.Bundle;
+import java.time.Duration;
+
+/** Activity that exercises Duration.toSeconds() to test core library desugaring. */
+public class DesugaringActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Force the compiler to retain the call to DurationUser.getSeconds
+ long seconds = DurationUser.getSeconds(Duration.ofMinutes(5));
+ setTitle("Seconds: " + seconds);
+ }
+}
diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/DurationUser.java b/test/rules/android_binary/r8_integration/java/com/desugaring/DurationUser.java
new file mode 100644
index 000000000..2c63bec78
--- /dev/null
+++ b/test/rules/android_binary/r8_integration/java/com/desugaring/DurationUser.java
@@ -0,0 +1,18 @@
+package com.desugaring;
+
+import java.time.Duration;
+
+/**
+ * A class that uses Duration.toSeconds(), which was added in API 31.
+ * This simulates a third-party library (like Google Nav SDK) that calls
+ * methods not available on all supported API levels.
+ *
+ * Without core library desugaring, this causes NoSuchMethodError on
+ * API 26-30 devices.
+ */
+public class DurationUser {
+ public static long getSeconds(Duration duration) {
+ // Duration.toSeconds() requires API 31+
+ return duration.toSeconds();
+ }
+}
diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/proguard.cfg b/test/rules/android_binary/r8_integration/java/com/desugaring/proguard.cfg
new file mode 100644
index 000000000..7ef29b7a9
--- /dev/null
+++ b/test/rules/android_binary/r8_integration/java/com/desugaring/proguard.cfg
@@ -0,0 +1,2 @@
+-dontobfuscate
+-keep class com.desugaring.DurationUser { *; }
diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/res/layout/activity_main.xml b/test/rules/android_binary/r8_integration/java/com/desugaring/res/layout/activity_main.xml
new file mode 100644
index 000000000..bce65f74a
--- /dev/null
+++ b/test/rules/android_binary/r8_integration/java/com/desugaring/res/layout/activity_main.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/test/rules/android_binary/r8_integration/java/com/desugaring/res/values/strings.xml b/test/rules/android_binary/r8_integration/java/com/desugaring/res/values/strings.xml
new file mode 100644
index 000000000..a5efdad92
--- /dev/null
+++ b/test/rules/android_binary/r8_integration/java/com/desugaring/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Desugaring Test
+
diff --git a/test/rules/android_binary/r8_integration/r8_desugaring_test.py b/test/rules/android_binary/r8_integration/r8_desugaring_test.py
new file mode 100644
index 000000000..a990b2c6c
--- /dev/null
+++ b/test/rules/android_binary/r8_integration/r8_desugaring_test.py
@@ -0,0 +1,80 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Tests that R8 properly applies core library desugaring.
+
+Verifies that methods added after API 26 (like Duration.toSeconds() from
+API 31) are retargeted to their backported implementations when R8 processes
+an android_binary with core library desugaring enabled.
+"""
+
+import os
+import subprocess
+import sys
+import unittest
+import zipfile
+
+
+class R8DesugaringTest(unittest.TestCase):
+ """Tests R8 core library desugaring integration."""
+
+ def _get_dexdump_output(self, apk_name):
+ tmp = os.environ["TEST_TMPDIR"]
+ apk_directory = "test/rules/android_binary/r8_integration/java/com/desugaring"
+ apk_path = os.path.join(apk_directory, apk_name)
+ apk_tmp = os.path.join(tmp, apk_name)
+
+ all_output = []
+ with zipfile.ZipFile(apk_path) as zf:
+ for name in zf.namelist():
+ if name.endswith(".dex"):
+ zf.extract(name, apk_tmp)
+ dex_path = os.path.join(apk_tmp, name)
+ proc = subprocess.run(
+ [dexdump, "-d", dex_path],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=True,
+ )
+ all_output.append(proc.stdout.decode(errors="replace"))
+
+ return "\n".join(all_output)
+
+ def test_duration_to_seconds_is_desugared(self):
+ """Duration.toSeconds() (API 31) must not appear as a raw call in the DEX."""
+ output = self._get_dexdump_output("desugaring_app_r8.apk")
+
+ self.assertNotIn(
+ "Ljava/time/Duration;.toSeconds:()J",
+ output,
+ "Duration.toSeconds() was NOT desugared. This method requires API 31 "
+ "and will cause NoSuchMethodError on API 28-30 devices. "
+ "R8 must be passed --desugared-lib to retarget this call.",
+ )
+
+ def test_desugared_duration_class_present(self):
+ """The desugared library runtime must be included in the APK."""
+ output = self._get_dexdump_output("desugaring_app_r8.apk")
+
+ # The DurationUser class should still be in the DEX (kept by proguard rules)
+ self.assertIn(
+ "Class descriptor : 'Lcom/desugaring/DurationUser;'",
+ output,
+ "DurationUser class not found in DEX output",
+ )
+
+
+if __name__ == "__main__":
+ dexdump = sys.argv.pop()
+ unittest.main(argv=None)