@JavaDerive(toString=true) for enum types

Enum types are translated into @interface class. When annotated with
@JavaDerive(toString=true), toString() method is generated in a nested
utility class "$".

For example, "Enum" type in AIDL will look like in the Java

  @interface Enum {
     public static final int FOO = 0;
     interface $ {
       static String toString(int v) { ... }
       static String arrayToString(Object v) { ... }
  }

arrayToString() will print enum arrays just like Arrays.toString().

Parcelable's toString() will use enum's toString when the enum type is
annotated with @JavaDerive(toString=true) as well.

Bug: 225289741
Test: aidl_integration_test
Change-Id: I854b6cbc52dbfdb78201a2cc5c27027ee93a7c9d
diff --git a/aidl_language.cpp b/aidl_language.cpp
index 62bde98..6e19d6e 100644
--- a/aidl_language.cpp
+++ b/aidl_language.cpp
@@ -154,7 +154,7 @@
        /* repeatable= */ true},
       {AidlAnnotation::Type::JAVA_DERIVE,
        "JavaDerive",
-       CONTEXT_TYPE_STRUCTURED_PARCELABLE | CONTEXT_TYPE_UNION,
+       CONTEXT_TYPE_STRUCTURED_PARCELABLE | CONTEXT_TYPE_UNION | CONTEXT_TYPE_ENUM,
        {{"toString", kBooleanType}, {"equals", kBooleanType}}},
       {AidlAnnotation::Type::JAVA_DEFAULT, "JavaDefault", CONTEXT_TYPE_INTERFACE, {}},
       {AidlAnnotation::Type::JAVA_DELEGATOR, "JavaDelegator", CONTEXT_TYPE_INTERFACE, {}},
diff --git a/aidl_to_java.cpp b/aidl_to_java.cpp
index 3cc2472..dd74660 100644
--- a/aidl_to_java.cpp
+++ b/aidl_to_java.cpp
@@ -135,14 +135,14 @@
 }
 
 namespace {
-string JavaSignatureOfInternal(
-    const AidlTypeSpecifier& aidl, bool instantiable, bool omit_array,
-    bool boxing = false /* boxing can be true only if it is a type parameter */) {
+string JavaSignatureOfInternal(const AidlTypeSpecifier& aidl, bool instantiable, bool omit_array,
+                               bool boxing) {
   string ret = JavaNameOf(aidl, instantiable, boxing && !aidl.IsArray());
   if (aidl.IsGeneric()) {
     vector<string> arg_names;
     for (const auto& ta : aidl.GetTypeParameters()) {
-      arg_names.emplace_back(JavaSignatureOfInternal(*ta, false, false, true /* boxing */));
+      arg_names.emplace_back(JavaSignatureOfInternal(*ta, /*instantiable=*/false,
+                                                     /*omit_array=*/false, /*boxing=*/true));
     }
     ret += "<" + Join(arg_names, ",") + ">";
   }
@@ -185,11 +185,20 @@
 }  // namespace
 
 string JavaSignatureOf(const AidlTypeSpecifier& aidl) {
-  return JavaSignatureOfInternal(aidl, false, false);
+  return JavaSignatureOfInternal(aidl, /*instantiable=*/false, /*omit_array=*/false,
+                                 /*boxing=*/false);
 }
 
+// Used for "new" expression. Ignore arrays because "new" expression handles it.
 string InstantiableJavaSignatureOf(const AidlTypeSpecifier& aidl) {
-  return JavaSignatureOfInternal(aidl, true, true);
+  return JavaSignatureOfInternal(aidl, /*instantiable=*/true, /*omit_array=*/true,
+                                 /*boxing=*/false);
+}
+
+string JavaBoxingTypeOf(const AidlTypeSpecifier& aidl) {
+  AIDL_FATAL_IF(!AidlTypenames::IsPrimitiveTypename(aidl.GetName()), aidl);
+  return JavaSignatureOfInternal(aidl, /*instantiable=*/false, /*omit_array=*/false,
+                                 /*boxing=*/true);
 }
 
 string DefaultJavaValueOf(const AidlTypeSpecifier& aidl) {
@@ -893,6 +902,17 @@
 }
 
 void ToStringFor(const CodeGeneratorContext& c) {
+  // Use derived toString() for enum type annotated with @JavaDerive(toString=true)
+  if (auto t = c.type.GetDefinedType();
+      t != nullptr && t->AsEnumDeclaration() && t->JavaDerive("toString")) {
+    if (c.type.IsArray()) {
+      c.writer << c.type.GetName() << ".$.arrayToString(" << c.var << ")";
+    } else {
+      c.writer << c.type.GetName() << ".$.toString(" << c.var << ")";
+    }
+    return;
+  }
+
   if (c.type.IsArray()) {
     if (c.type.IsDynamicArray() || c.type.GetFixedSizeArrayDimensions().size() == 1) {
       c.writer << "java.util.Arrays.toString(" << c.var << ")";
diff --git a/aidl_to_java.h b/aidl_to_java.h
index 5a4632c..01634f4 100644
--- a/aidl_to_java.h
+++ b/aidl_to_java.h
@@ -47,6 +47,10 @@
 // This includes generic type parameters with array modifiers.
 string JavaSignatureOf(const AidlTypeSpecifier& aidl);
 
+// Returns the Java boxing type of the AIDL type spec.
+// aidl type should be a primitive type.
+string JavaBoxingTypeOf(const AidlTypeSpecifier& aidl);
+
 // Returns the instantiable Jva type signature of the AIDL type spec
 // This includes generic type parameters, but excludes array modifiers.
 string InstantiableJavaSignatureOf(const AidlTypeSpecifier& aidl);
diff --git a/aidl_unittest.cpp b/aidl_unittest.cpp
index 4f94b16..54677df 100644
--- a/aidl_unittest.cpp
+++ b/aidl_unittest.cpp
@@ -620,14 +620,6 @@
     EXPECT_FALSE(compile_aidl(java_options, io_delegate_));
     EXPECT_THAT(GetCapturedStderr(), HasSubstr("@JavaDerive is not available."));
   }
-
-  {
-    io_delegate_.SetFileContents("a/IFoo.aidl", "package a; @JavaDerive enum IFoo { A=1, }");
-    Options java_options = Options::From("aidl --lang=java -o out a/IFoo.aidl");
-    CaptureStderr();
-    EXPECT_FALSE(compile_aidl(java_options, io_delegate_));
-    EXPECT_THAT(GetCapturedStderr(), HasSubstr("@JavaDerive is not available."));
-  }
 }
 
 TEST_P(AidlTest, ParseDescriptorAnnotation) {
diff --git a/generate_java.cpp b/generate_java.cpp
index 8dc1643..9ba025f 100644
--- a/generate_java.cpp
+++ b/generate_java.cpp
@@ -603,6 +603,9 @@
 }
 
 void GenerateEnumClass(CodeWriter& out, const AidlEnumDeclaration& enum_decl) {
+  const AidlTypeSpecifier& backing_type = enum_decl.GetBackingType();
+  std::string raw_type = JavaSignatureOf(backing_type);
+  std::string boxing_type = JavaBoxingTypeOf(backing_type);
   out << GenerateComments(enum_decl);
   out << GenerateAnnotations(enum_decl);
   out << "public ";
@@ -614,9 +617,43 @@
   for (const auto& enumerator : enum_decl.GetEnumerators()) {
     out << GenerateComments(*enumerator);
     out << GenerateAnnotations(*enumerator);
-    out << fmt::format("public static final {} {} = {};\n",
-                       JavaSignatureOf(enum_decl.GetBackingType()), enumerator->GetName(),
-                       enumerator->ValueString(enum_decl.GetBackingType(), ConstantValueDecorator));
+    out << fmt::format("public static final {} {} = {};\n", raw_type, enumerator->GetName(),
+                       enumerator->ValueString(backing_type, ConstantValueDecorator));
+  }
+  if (enum_decl.JavaDerive("toString")) {
+    out << "interface $ {\n";
+    out.Indent();
+    out << "static String toString(" << raw_type << " _aidl_v) {\n";
+    out.Indent();
+    for (const auto& enumerator : enum_decl.GetEnumerators()) {
+      out << "if (_aidl_v == " << enumerator->GetName() << ") return \"" << enumerator->GetName()
+          << "\";\n";
+    }
+    out << "return " << boxing_type << ".toString(_aidl_v);\n";
+    out.Dedent();
+    out << "}\n";
+    out << fmt::format(R"(static String arrayToString(Object _aidl_v) {{
+  if (_aidl_v == null) return "null";
+  Class<?> _aidl_cls = _aidl_v.getClass();
+  if (!_aidl_cls.isArray()) throw new IllegalArgumentException("not an array: " + _aidl_v);
+  Class<?> comp = _aidl_cls.getComponentType();
+  java.util.StringJoiner _aidl_sj = new java.util.StringJoiner(", ", "[", "]");
+  if (comp.isArray()) {{
+    for (int _aidl_i = 0; _aidl_i < java.lang.reflect.Array.getLength(_aidl_v); _aidl_i++) {{
+      _aidl_sj.add(arrayToString(java.lang.reflect.Array.get(_aidl_v, _aidl_i)));
+    }}
+  }} else {{
+    if (_aidl_cls != {raw_type}[].class) throw new IllegalArgumentException("wrong type: " + _aidl_cls);
+    for ({raw_type} e : ({raw_type}[]) _aidl_v) {{
+      _aidl_sj.add(toString(e));
+    }}
+  }}
+  return _aidl_sj.toString();
+}}
+)",
+                       fmt::arg("raw_type", raw_type));
+    out.Dedent();
+    out << "}\n";
   }
   out.Dedent();
   out << "}\n";
diff --git a/tests/android/aidl/tests/IntEnum.aidl b/tests/android/aidl/tests/IntEnum.aidl
index b241871..7ab63f3 100644
--- a/tests/android/aidl/tests/IntEnum.aidl
+++ b/tests/android/aidl/tests/IntEnum.aidl
@@ -16,6 +16,7 @@
 
 package android.aidl.tests;
 
+@JavaDerive(toString=true)
 @Backing(type="int")
 enum IntEnum {
     FOO = 1000,
diff --git a/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/IntEnum.java b/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/IntEnum.java
index 0e2dd06..4114fe7 100644
--- a/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/IntEnum.java
+++ b/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/IntEnum.java
@@ -9,4 +9,31 @@
   /** @deprecated do not use this */
   @Deprecated
   public static final int QUX = 2002;
+  interface $ {
+    static String toString(int _aidl_v) {
+      if (_aidl_v == FOO) return "FOO";
+      if (_aidl_v == BAR) return "BAR";
+      if (_aidl_v == BAZ) return "BAZ";
+      if (_aidl_v == QUX) return "QUX";
+      return Integer.toString(_aidl_v);
+    }
+    static String arrayToString(Object _aidl_v) {
+      if (_aidl_v == null) return "null";
+      Class<?> _aidl_cls = _aidl_v.getClass();
+      if (!_aidl_cls.isArray()) throw new IllegalArgumentException("not an array: " + _aidl_v);
+      Class<?> comp = _aidl_cls.getComponentType();
+      java.util.StringJoiner _aidl_sj = new java.util.StringJoiner(", ", "[", "]");
+      if (comp.isArray()) {
+        for (int _aidl_i = 0; _aidl_i < java.lang.reflect.Array.getLength(_aidl_v); _aidl_i++) {
+          _aidl_sj.add(arrayToString(java.lang.reflect.Array.get(_aidl_v, _aidl_i)));
+        }
+      } else {
+        if (_aidl_cls != int[].class) throw new IllegalArgumentException("wrong type: " + _aidl_cls);
+        for (int e : (int[]) _aidl_v) {
+          _aidl_sj.add(toString(e));
+        }
+      }
+      return _aidl_sj.toString();
+    }
+  }
 }
diff --git a/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/ParcelableForToString.java b/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/ParcelableForToString.java
index c588be1..03ce5be 100644
--- a/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/ParcelableForToString.java
+++ b/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/ParcelableForToString.java
@@ -150,8 +150,8 @@
     _aidl_sj.add("stringList: " + (java.util.Objects.toString(stringList)));
     _aidl_sj.add("parcelableValue: " + (java.util.Objects.toString(parcelableValue)));
     _aidl_sj.add("parcelableArray: " + (java.util.Arrays.toString(parcelableArray)));
-    _aidl_sj.add("enumValue: " + (enumValue));
-    _aidl_sj.add("enumArray: " + (java.util.Arrays.toString(enumArray)));
+    _aidl_sj.add("enumValue: " + (android.aidl.tests.IntEnum.$.toString(enumValue)));
+    _aidl_sj.add("enumArray: " + (android.aidl.tests.IntEnum.$.arrayToString(enumArray)));
     _aidl_sj.add("nullArray: " + (java.util.Arrays.toString(nullArray)));
     _aidl_sj.add("nullList: " + (java.util.Objects.toString(nullList)));
     _aidl_sj.add("parcelableGeneric: " + (java.util.Objects.toString(parcelableGeneric)));
diff --git a/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/StructuredParcelable.java b/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/StructuredParcelable.java
index b90fe2c..3d7685a 100644
--- a/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/StructuredParcelable.java
+++ b/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/StructuredParcelable.java
@@ -272,10 +272,10 @@
     _aidl_sj.add("f: " + (f));
     _aidl_sj.add("shouldBeJerry: " + (java.util.Objects.toString(shouldBeJerry)));
     _aidl_sj.add("shouldBeByteBar: " + (shouldBeByteBar));
-    _aidl_sj.add("shouldBeIntBar: " + (shouldBeIntBar));
+    _aidl_sj.add("shouldBeIntBar: " + (android.aidl.tests.IntEnum.$.toString(shouldBeIntBar)));
     _aidl_sj.add("shouldBeLongBar: " + (shouldBeLongBar));
     _aidl_sj.add("shouldContainTwoByteFoos: " + (java.util.Arrays.toString(shouldContainTwoByteFoos)));
-    _aidl_sj.add("shouldContainTwoIntFoos: " + (java.util.Arrays.toString(shouldContainTwoIntFoos)));
+    _aidl_sj.add("shouldContainTwoIntFoos: " + (android.aidl.tests.IntEnum.$.arrayToString(shouldContainTwoIntFoos)));
     _aidl_sj.add("shouldContainTwoLongFoos: " + (java.util.Arrays.toString(shouldContainTwoLongFoos)));
     _aidl_sj.add("stringDefaultsToFoo: " + (java.util.Objects.toString(stringDefaultsToFoo)));
     _aidl_sj.add("byteDefaultsToFour: " + (byteDefaultsToFour));
@@ -322,7 +322,7 @@
     _aidl_sj.add("shouldSetBit0AndBit2: " + (shouldSetBit0AndBit2));
     _aidl_sj.add("u: " + (java.util.Objects.toString(u)));
     _aidl_sj.add("shouldBeConstS1: " + (java.util.Objects.toString(shouldBeConstS1)));
-    _aidl_sj.add("defaultWithFoo: " + (defaultWithFoo));
+    _aidl_sj.add("defaultWithFoo: " + (android.aidl.tests.IntEnum.$.toString(defaultWithFoo)));
     return "android.aidl.tests.StructuredParcelable" + _aidl_sj.toString()  ;
   }
   @Override
diff --git a/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/unions/EnumUnion.java b/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/unions/EnumUnion.java
index 038d813..937d9d5 100644
--- a/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/unions/EnumUnion.java
+++ b/tests/golden_output/aidl-test-interface-java-source/gen/android/aidl/tests/unions/EnumUnion.java
@@ -112,7 +112,7 @@
   @Override
   public String toString() {
     switch (_tag) {
-    case intEnum: return "android.aidl.tests.unions.EnumUnion.intEnum(" + (getIntEnum()) + ")";
+    case intEnum: return "android.aidl.tests.unions.EnumUnion.intEnum(" + (android.aidl.tests.IntEnum.$.toString(getIntEnum())) + ")";
     case longEnum: return "android.aidl.tests.unions.EnumUnion.longEnum(" + (getLongEnum()) + ")";
     }
     throw new IllegalStateException("unknown field: " + _tag);
diff --git a/tests/java/src/android/aidl/tests/TestServiceClient.java b/tests/java/src/android/aidl/tests/TestServiceClient.java
index 6dfacc9..098ad20 100644
--- a/tests/java/src/android/aidl/tests/TestServiceClient.java
+++ b/tests/java/src/android/aidl/tests/TestServiceClient.java
@@ -757,10 +757,10 @@
             + "f: 17, "
             + "shouldBeJerry: Jerry, "
             + "shouldBeByteBar: 2, "
-            + "shouldBeIntBar: 2000, "
+            + "shouldBeIntBar: BAR, "
             + "shouldBeLongBar: 200000000000, "
             + "shouldContainTwoByteFoos: [1, 1], "
-            + "shouldContainTwoIntFoos: [1000, 1000], "
+            + "shouldContainTwoIntFoos: [FOO, FOO], "
             + "shouldContainTwoLongFoos: [100000000000, 100000000000], "
             + "stringDefaultsToFoo: foo, "
             + "byteDefaultsToFour: 4, "
@@ -809,7 +809,7 @@
             + "shouldSetBit0AndBit2: 5, "
             + "u: android.aidl.tests.Union.ns([1, 2, 3]), "
             + "shouldBeConstS1: android.aidl.tests.Union.s(a string constant in union), "
-            + "defaultWithFoo: 1000"
+            + "defaultWithFoo: FOO"
             + "}";
         assertThat(p.toString(), is(expected));
     }
@@ -888,8 +888,8 @@
             + "parcelableArray: ["
             + "android.aidl.tests.OtherParcelableForToString{field: other}, "
             + "android.aidl.tests.OtherParcelableForToString{field: other}], "
-            + "enumValue: 1000, "
-            + "enumArray: [1000, 2000], "
+            + "enumValue: FOO, "
+            + "enumArray: [FOO, BAR], "
             + "nullArray: null, "
             + "nullList: null, "
             + "parcelableGeneric: android.aidl.tests.GenericStructuredParcelable{a: 1, b: 2}, "
@@ -900,6 +900,21 @@
     }
 
     @Test
+    public void testEnumToString() {
+      assertThat(IntEnum.$.toString(IntEnum.FOO), is("FOO"));
+      assertThat(IntEnum.$.toString(0), is("0"));
+      assertThat(IntEnum.$.arrayToString(null), is("null"));
+      assertThat(IntEnum.$.arrayToString(new int[] {}), is("[]"));
+      assertThat(IntEnum.$.arrayToString(new int[] {IntEnum.FOO, IntEnum.BAR}), is("[FOO, BAR]"));
+      assertThat(IntEnum.$.arrayToString(new int[] {IntEnum.FOO, 0}), is("[FOO, 0]"));
+      assertThat(IntEnum.$.arrayToString(new int[][] {{IntEnum.FOO, IntEnum.BAR}, {IntEnum.BAZ}}),
+          is("[[FOO, BAR], [BAZ]]"));
+      assertThrows(IllegalArgumentException.class, () -> IntEnum.$.arrayToString(IntEnum.FOO));
+      assertThrows(
+          IllegalArgumentException.class, () -> IntEnum.$.arrayToString(new long[] {LongEnum.FOO}));
+    }
+
+    @Test
     public void testRenamedInterface() throws RemoteException {
       IOldName oldAsOld = service.GetOldNameInterface();
       assertNotNull(oldAsOld);