Support Duration#isPositive

- Always supported with desugared libraryi (except minimal)
- Supported above or equal api 26

Fixes: b/389751852
Change-Id: I9f629b381d8ad444490511bed0d613738c522c9a
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 4d8c7c5..5ec1ce3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -502,6 +502,7 @@
   public final DexType mapEntryType = createStaticallyKnownType(mapEntryDescriptor);
   public final DexType abstractMapSimpleEntryType =
       createStaticallyKnownType("Ljava/util/AbstractMap$SimpleEntry;");
+  public final DexType durationType = createStaticallyKnownType("Ljava/time/Duration;");
   public final DexType collectionType = createStaticallyKnownType(collectionDescriptor);
   public final DexType comparatorType = createStaticallyKnownType(comparatorDescriptor);
   public final DexType callableType = createStaticallyKnownType(callableDescriptor);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
index d7aff3c..048d9a3 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -364,6 +364,9 @@
       if (options.getMinApiLevel().isLessThan(AndroidApiLevel.BAKLAVA)) {
         initializeAndroidBaklavaMethodProviders(factory);
       }
+      if (typeIsPresent(factory.durationType)) {
+        initializeDurationMethodProviders(factory);
+      }
     }
 
     private Map<DexType, AndroidApiLevel> initializeTypeMinApi(DexItemFactory factory) {
@@ -374,6 +377,7 @@
       builder.put(factory.javaUtilSetType, AndroidApiLevel.B);
       builder.put(factory.streamType, AndroidApiLevel.N);
       builder.put(factory.supplierType, AndroidApiLevel.N);
+      builder.put(factory.durationType, AndroidApiLevel.O);
       ImmutableMap<DexType, AndroidApiLevel> typeMinApi = builder.build();
       assert minApiMatchDatabaseMinApi(typeMinApi);
       return typeMinApi;
@@ -1777,6 +1781,17 @@
       }
     }
 
+    private void initializeDurationMethodProviders(DexItemFactory factory) {
+      // boolean Duration.isPositive()
+      DexType type = factory.durationType;
+      DexString name = factory.createString("isPositive");
+      DexProto proto = factory.createProto(factory.booleanType);
+      DexMethod method = factory.createMethod(type, proto, name);
+      addProvider(
+          new StatifyingMethodGenerator(
+              method, BackportedMethods::DurationMethods_isPositive, "isPositive", type));
+    }
+
     private void initializeAndroidBaklavaMethodProviders(DexItemFactory factory) {
       DexType type;
       DexString name;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethods.java b/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethods.java
index 6c3f005..6aca418 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethods.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethods.java
@@ -85,6 +85,7 @@
     factory.createSynthesizedType("Ljava/lang/reflect/Method;");
     factory.createSynthesizedType("Ljava/math/BigDecimal;");
     factory.createSynthesizedType("Ljava/math/BigInteger;");
+    factory.createSynthesizedType("Ljava/time/Duration;");
     factory.createSynthesizedType("Ljava/util/AbstractMap$SimpleImmutableEntry;");
     factory.createSynthesizedType("Ljava/util/ArrayList;");
     factory.createSynthesizedType("Ljava/util/Arrays;");
@@ -2413,6 +2414,61 @@
         ImmutableList.of());
   }
 
+  public static CfCode DurationMethods_isPositive(DexItemFactory factory, DexMethod method) {
+    CfLabel label0 = new CfLabel();
+    CfLabel label1 = new CfLabel();
+    CfLabel label2 = new CfLabel();
+    CfLabel label3 = new CfLabel();
+    return new CfCode(
+        method.holder,
+        1,
+        1,
+        ImmutableList.of(
+            label0,
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.createType("Ljava/time/Duration;"),
+                    factory.createProto(factory.booleanType),
+                    factory.createString("isZero")),
+                false),
+            new CfIf(IfType.NE, ValueType.INT, label1),
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfInvoke(
+                182,
+                factory.createMethod(
+                    factory.createType("Ljava/time/Duration;"),
+                    factory.createProto(factory.booleanType),
+                    factory.createString("isNegative")),
+                false),
+            new CfIf(IfType.NE, ValueType.INT, label1),
+            new CfConstNumber(1, ValueType.INT),
+            new CfGoto(label2),
+            label1,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Ljava/time/Duration;"))
+                    })),
+            new CfConstNumber(0, ValueType.INT),
+            label2,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(
+                    new int[] {0},
+                    new FrameType[] {
+                      FrameType.initializedNonNullReference(
+                          factory.createType("Ljava/time/Duration;"))
+                    }),
+                new ArrayDeque<>(Arrays.asList(FrameType.intType()))),
+            new CfReturn(ValueType.INT),
+            label3),
+        ImmutableList.of(),
+        ImmutableList.of());
+  }
+
   public static CfCode ExecutorServiceMethods_closeExecutorService(
       DexItemFactory factory, DexMethod method) {
     CfLabel label0 = new CfLabel();
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/backports/DurationMethods.java b/src/test/java/com/android/tools/r8/ir/desugar/backports/DurationMethods.java
new file mode 100644
index 0000000..6aeb796
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/backports/DurationMethods.java
@@ -0,0 +1,14 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.backports;
+
+import java.time.Duration;
+
+public class DurationMethods {
+
+  public static boolean isPositive(Duration duration) {
+    return !duration.isZero() && !duration.isNegative();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/backports/GenerateBackportMethods.java b/src/test/java/com/android/tools/r8/ir/desugar/backports/GenerateBackportMethods.java
index 40d6b6d..100b640 100644
--- a/src/test/java/com/android/tools/r8/ir/desugar/backports/GenerateBackportMethods.java
+++ b/src/test/java/com/android/tools/r8/ir/desugar/backports/GenerateBackportMethods.java
@@ -50,6 +50,7 @@
           CollectionMethods.class,
           CollectionsMethods.class,
           DoubleMethods.class,
+          DurationMethods.class,
           ExecutorServiceMethods.class,
           FloatMethods.class,
           IntegerMethods.class,
diff --git a/src/test/java21/com/android/tools/r8/jdk21/desugaredlibrary/DurationIsPositiveTest.java b/src/test/java21/com/android/tools/r8/jdk21/desugaredlibrary/DurationIsPositiveTest.java
new file mode 100644
index 0000000..b5c6800
--- /dev/null
+++ b/src/test/java21/com/android/tools/r8/jdk21/desugaredlibrary/DurationIsPositiveTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.jdk21.desugaredlibrary;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8DEBUG;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.DEFAULT_SPECIFICATIONS;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.time.Duration;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DurationIsPositiveTest extends DesugaredLibraryTestBase {
+
+  private static final String EXPECTED_RESULT = StringUtils.lines("true", "false", "false");
+
+  private final TestParameters parameters;
+  private final LibraryDesugaringSpecification libraryDesugaringSpecification;
+  private final CompilationSpecification compilationSpecification;
+
+  @Parameters(name = "{0}, spec: {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimesAndAllApiLevels().build(),
+        ImmutableList.of(JDK11, JDK11_PATH),
+        DEFAULT_SPECIFICATIONS);
+  }
+
+  public DurationIsPositiveTest(
+      TestParameters parameters,
+      LibraryDesugaringSpecification libraryDesugaringSpecification,
+      CompilationSpecification compilationSpecification) {
+    this.parameters = parameters;
+    this.libraryDesugaringSpecification = libraryDesugaringSpecification;
+    this.compilationSpecification = compilationSpecification;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    Assume.assumeTrue(
+        "Run only once",
+        libraryDesugaringSpecification == JDK11 && compilationSpecification == D8_L8DEBUG);
+    testForD8()
+        .addInnerClassesAndStrippedOuter(getClass())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Executor.class)
+        .applyIf(
+            parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O),
+            b -> b.assertSuccessWithOutput(EXPECTED_RESULT),
+            parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V8_1_0),
+            b -> b.assertFailureWithErrorThatThrows(NoSuchMethodError.class),
+            b -> b.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
+  }
+
+  @Test
+  public void testDesugaredLib() throws Exception {
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addInnerClassesAndStrippedOuter(getClass())
+        .addKeepMainRule(Executor.class)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  static class Executor {
+    public static void main(String[] args) {
+      System.out.println(Duration.ofDays(5).isPositive());
+      System.out.println(Duration.ofDays(-3).isPositive());
+      System.out.println(Duration.ofDays(0).isPositive());
+    }
+  }
+}