Backport String.join methods

Test: tools/test.py --dex_vm all --no-internal *BackportedMethodRewriterTest*
Change-Id: I9610140661f82cdb8f9f89c3d08ceaa176dd1c32
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 79e15c5..a476e44 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -179,6 +179,7 @@
   public final DexString invokeExactMethodName = createString("invokeExact");
 
   public final DexString charSequenceDescriptor = createString("Ljava/lang/CharSequence;");
+  public final DexString charSequenceArrayDescriptor = createString("[Ljava/lang/CharSequence;");
   public final DexString stringDescriptor = createString("Ljava/lang/String;");
   public final DexString stringArrayDescriptor = createString("[Ljava/lang/String;");
   public final DexString objectDescriptor = createString("Ljava/lang/Object;");
@@ -193,6 +194,7 @@
   public final DexString enumDescriptor = createString("Ljava/lang/Enum;");
   public final DexString annotationDescriptor = createString("Ljava/lang/annotation/Annotation;");
   public final DexString objectsDescriptor = createString("Ljava/util/Objects;");
+  public final DexString iterableDescriptor = createString("Ljava/lang/Iterable;");
 
   public final DexString stringBuilderDescriptor = createString("Ljava/lang/StringBuilder;");
   public final DexString stringBufferDescriptor = createString("Ljava/lang/StringBuffer;");
@@ -256,6 +258,7 @@
   public final DexType boxedNumberType = createType(boxedNumberDescriptor);
 
   public final DexType charSequenceType = createType(charSequenceDescriptor);
+  public final DexType charSequenceArrayType = createType(charSequenceArrayDescriptor);
   public final DexType stringType = createType(stringDescriptor);
   public final DexType stringArrayType = createType(stringArrayDescriptor);
   public final DexType objectType = createType(objectDescriptor);
@@ -263,6 +266,7 @@
   public final DexType classArrayType = createType(classArrayDescriptor);
   public final DexType enumType = createType(enumDescriptor);
   public final DexType annotationType = createType(annotationDescriptor);
+  public final DexType iterableType = createType(iterableDescriptor);
 
   public final DexType classType = createType(classDescriptor);
   public final DexType classLoaderType = createType(classLoaderDescriptor);
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 11a92d6..9e80a2e 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
@@ -38,6 +38,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -456,6 +457,40 @@
     }
   }
 
+  private static final class StringMethods extends TemplateMethodCode {
+    StringMethods(InternalOptions options, DexMethod method, String methodName) {
+      super(options, method, methodName, method.proto.toDescriptorString());
+    }
+
+    public static String joinArrayImpl(CharSequence delimiter, CharSequence... elements) {
+      if (delimiter == null) throw new NullPointerException("delimiter");
+      StringBuilder builder = new StringBuilder();
+      if (elements.length > 0) {
+        builder.append(elements[0]);
+        for (int i = 1; i < elements.length; i++) {
+          builder.append(delimiter);
+          builder.append(elements[i]);
+        }
+      }
+      return builder.toString();
+    }
+
+    public static String joinIterableImpl(CharSequence delimiter,
+        Iterable<? extends CharSequence> elements) {
+      if (delimiter == null) throw new NullPointerException("delimiter");
+      StringBuilder builder = new StringBuilder();
+      Iterator<? extends CharSequence> iterator = elements.iterator();
+      if (iterator.hasNext()) {
+        builder.append(iterator.next());
+        while (iterator.hasNext()) {
+          builder.append(delimiter);
+          builder.append(iterator.next());
+        }
+      }
+      return builder.toString();
+    }
+  }
+
   private static final class ObjectsMethods extends TemplateMethodCode {
     ObjectsMethods(InternalOptions options, DexMethod method, String methodName) {
       super(options, method, methodName, method.proto.toDescriptorString());
@@ -922,6 +957,23 @@
       proto = factory.createProto(factory.intType, factory.longType, factory.longType);
       addOrGetMethod(clazz, method).put(proto,
           new MethodGenerator(LongMethods::new, "compareUnsignedImpl", clazz, method, proto));
+
+      // String
+      clazz = factory.stringDescriptor;
+
+      // String String.join(CharSequence, CharSequence...)
+      method = factory.createString("join");
+      proto = factory.createProto(factory.stringType, factory.charSequenceType,
+          factory.charSequenceArrayType);
+      addOrGetMethod(clazz, method).put(proto,
+          new MethodGenerator(StringMethods::new, "joinArrayImpl", clazz, method, proto));
+
+      // String String.join(CharSequence, Iterable<? extends CharSequence>)
+      method = factory.createString("join");
+      proto =
+          factory.createProto(factory.stringType, factory.charSequenceType, factory.iterableType);
+      addOrGetMethod(clazz, method).put(proto,
+          new MethodGenerator(StringMethods::new, "joinIterableImpl", clazz, method, proto));
     }
 
     private Map<DexString, Map<DexProto, MethodGenerator>> addOrGetClass(DexString clazz) {
diff --git a/src/test/java/com/android/tools/r8/desugar/BackportedMethodRewriterTest.java b/src/test/java/com/android/tools/r8/desugar/BackportedMethodRewriterTest.java
index 5a77115..5eb581d 100644
--- a/src/test/java/com/android/tools/r8/desugar/BackportedMethodRewriterTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/BackportedMethodRewriterTest.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -39,7 +40,7 @@
         .run(TestMethods.class)
         .assertSuccessWithOutput(expectedOutput);
 
-    assertDesugaring(AndroidApiLevel.O, 60);
+    assertDesugaring(AndroidApiLevel.O, 70);
     assertDesugaring(AndroidApiLevel.N, 49);
     assertDesugaring(AndroidApiLevel.K, 20);
     assertDesugaring(AndroidApiLevel.J_MR2, 0);
@@ -137,6 +138,12 @@
       return (int) Math.signum(value);
     }
 
+    // Defined as a static method on this class to avoid affecting invoke-static counts in main().
+    @SafeVarargs
+    private static <T> List<T> listOf(T... values) {
+      return Arrays.asList(values);
+    }
+
     public static void main(String[] args) {
       byte[] aBytes = new byte[]{42, 1, -1, Byte.MAX_VALUE, Byte.MIN_VALUE};
       for (byte aByte : aBytes) {
@@ -244,6 +251,30 @@
         }
       }
 
+      System.out.println(String.join(", "));
+      System.out.println(String.join(", ", "one", "two", "three"));
+      System.out.println(String.join("", "one", "two", "three"));
+      try {
+        throw new AssertionError(String.join(null, "one", "two", "three"));
+      } catch (NullPointerException expected) {
+      }
+      try {
+        throw new AssertionError(String.join(", ", (CharSequence[]) null));
+      } catch (NullPointerException expected) {
+      }
+
+      System.out.println(String.join(", ", listOf()));
+      System.out.println(String.join(", ", listOf("one", "two", "three")));
+      System.out.println(String.join("", listOf("one", "two", "three")));
+      try {
+        throw new AssertionError(String.join(null, listOf("one", "two", "three")));
+      } catch (NullPointerException expected) {
+      }
+      try {
+        throw new AssertionError(String.join(", ", (Iterable<CharSequence>) null));
+      } catch (NullPointerException expected) {
+      }
+
       System.out.println(Objects.compare("a", "b", reverse()));
       System.out.println(Objects.compare("b", "a", reverse()));
       System.out.println(Objects.compare("a", "a", reverse()));