Adding functionality to handle proguard when storing enums in databases.

Test: Baseline tests replicate compiler errors in EnumColumnTypeAdapterTest.java

Bug: 73132006
Change-Id: Ifa0a0bc496d63467a6b54c2c4d264c97a4bde0cd
diff --git a/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
index 3babfd8..ba1a146 100644
--- a/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
@@ -131,6 +131,9 @@
     val STRING = ClassName.get("java.lang", "String")
     val INTEGER = ClassName.get("java.lang", "Integer")
     val OPTIONAL = ClassName.get("java.util", "Optional")
+    val ILLEGAL_ARG_EXCEPTION = ClassName.get(
+        "java.lang", "IllegalArgumentException"
+    )
 }
 
 object GuavaBaseTypeNames {
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
index 1194b1d..090dfc8 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright 2020 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.
@@ -16,29 +16,38 @@
 
 package androidx.room.solver.types
 
+import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XType
+import androidx.room.ext.CommonTypeNames.ILLEGAL_ARG_EXCEPTION
 import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.S
 import androidx.room.ext.T
 import androidx.room.parser.SQLTypeAffinity.TEXT
 import androidx.room.solver.CodeGenScope
+import androidx.room.writer.ClassWriter
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import java.util.Locale
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.Modifier
 
 /**
  * Uses enum string representation.
  */
 class EnumColumnTypeAdapter(out: XType) :
     ColumnTypeAdapter(out, TEXT) {
-    private val enumTypeName = out.typeName
     override fun readFromCursor(
         outVarName: String,
         cursorVarName: String,
         indexVarName: String,
         scope: CodeGenScope
     ) {
+        val stringToEnumMethod = stringToEnumMethod(scope)
         scope.builder()
             .addStatement(
-                "$L = $T.valueOf($L.getString($L))", outVarName, enumTypeName,
-                cursorVarName,
-                indexVarName
+                "$L = $N($L.getString($L))",
+                outVarName, stringToEnumMethod, cursorVarName, indexVarName
             )
     }
 
@@ -48,12 +57,102 @@
         valueVarName: String,
         scope: CodeGenScope
     ) {
+        val enumToStringMethod = enumToStringMethod(scope)
         scope.builder().apply {
             beginControlFlow("if ($L == null)", valueVarName)
                 .addStatement("$L.bindNull($L)", stmtName, indexVarName)
             nextControlFlow("else")
-                .addStatement("$L.bindString($L, $L.name())", stmtName, indexVarName, valueVarName)
+                .addStatement(
+                    "$L.bindString($L, $N($L))",
+                    stmtName, indexVarName, enumToStringMethod, valueVarName
+                )
             endControlFlow()
         }
     }
-}
\ No newline at end of file
+
+    private fun enumToStringMethod(scope: CodeGenScope): MethodSpec {
+        return scope.writer.getOrCreateMethod(object :
+                ClassWriter.SharedMethodSpec(out.asTypeElement().name + "_enumToString") {
+                override fun getUniqueKey(): String {
+                    return "enumToString_" + out.typeName.toString()
+                }
+
+                override fun prepare(
+                    methodName: String,
+                    writer: ClassWriter,
+                    builder: MethodSpec.Builder
+                ) {
+                    builder.apply {
+                        addModifiers(Modifier.PRIVATE)
+                        returns(String::class.java)
+                        val param = ParameterSpec.builder(
+                            out.typeName, "_value", Modifier.FINAL
+                        ).build()
+                        addParameter(param)
+                        beginControlFlow("if ($N == null)", param)
+                        addStatement("return null")
+                        nextControlFlow("switch ($N)", param)
+                        getEnumConstantElements().forEach { enumConstant ->
+                            addStatement("case $L: return $S", enumConstant.name, enumConstant.name)
+                        }
+                        addStatement(
+                            "default: throw new $T($S)",
+                            ILLEGAL_ARG_EXCEPTION,
+                            "Can't convert ${param.name} to string, unknown enum value."
+                        )
+                        endControlFlow()
+                    }
+                }
+            })
+    }
+
+    private fun stringToEnumMethod(scope: CodeGenScope): MethodSpec {
+        return scope.writer.getOrCreateMethod(object :
+                ClassWriter.SharedMethodSpec(out.asTypeElement().name + "_stringToEnum") {
+                override fun getUniqueKey(): String {
+                    return out.typeName.toString()
+                }
+
+                override fun prepare(
+                    methodName: String,
+                    writer: ClassWriter,
+                    builder: MethodSpec.Builder
+                ) {
+                    builder.apply {
+                        addModifiers(Modifier.PRIVATE)
+                        returns(out.typeName)
+                        val param = ParameterSpec.builder(
+                            String::class.java, "_value", Modifier.FINAL
+                        ).build()
+                        addParameter(param)
+                        beginControlFlow("if ($N == null)", param)
+                        addStatement("return null")
+                        nextControlFlow("switch ($N)", param)
+                        getEnumConstantElements().forEach {
+                            enumConstant ->
+                            addStatement(
+                                "case $S: return $T.$L",
+                                enumConstant.name, out.typeName, enumConstant.name
+                            )
+                        }
+                        addStatement(
+                            "default: throw new $T($S)",
+                            ILLEGAL_ARG_EXCEPTION,
+                            "Can't convert ${param.name} to enum, unknown value."
+                        )
+                        endControlFlow()
+                    }
+                }
+            })
+    }
+
+    private fun getEnumConstantElements(): List<XFieldElement> {
+        // TODO: Switch below logic to use`getDeclaredFields` when the
+        //  functionality is available in the XTypeElement API
+        val typeElementFields = out.asTypeElement().getAllFieldsIncludingPrivateSupers()
+        return typeElementFields.filter {
+            // TODO: (b/173236324) Add kind to the X abstraction API to avoid using kindName()
+            ElementKind.ENUM_CONSTANT.toString().toLowerCase(Locale.US) == it.kindName()
+        }
+    }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java
index 4c0df94..94cccaf 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java
@@ -42,6 +42,7 @@
 public class EnumColumnTypeAdapterTest {
 
     private EnumColumnTypeAdapterDatabase mDb;
+    private EnumColumnTypeAdapterDatabase mDbComplex;
 
     @Entity
     public static class EntityWithEnum {
@@ -49,6 +50,12 @@
         public Long id;
         public Fruit fruit;
     }
+    @Entity
+    public static class ComplexEntityWithEnum {
+        @PrimaryKey
+        public Long id;
+        public Season mSeason;
+    }
 
     public enum Fruit {
         BANANA,
@@ -56,6 +63,19 @@
         WILDBERRY
     }
 
+    public enum Season {
+        SUMMER("Sunny"),
+        SPRING("Warm"),
+        WINTER("Cold"),
+        AUTUMN("Rainy");
+
+        private final String mSeason;
+
+        Season(String mSeason) {
+            this.mSeason = mSeason;
+        }
+    }
+
     @Dao
     public interface SampleDao {
         @Query("INSERT INTO EntityWithEnum (id, fruit) VALUES (:id, :fruit)")
@@ -65,9 +85,20 @@
         EntityWithEnum getValueWithId(long id);
     }
 
-    @Database(entities = {EntityWithEnum.class}, version = 1, exportSchema = false)
+    @Dao
+    public interface SampleDaoWithComplexEnum {
+        @Query("INSERT INTO ComplexEntityWithEnum (id, mSeason) VALUES (:id, :season)")
+        long insertComplex(long id, Season season);
+
+        @Query("SELECT * FROM ComplexEntityWithEnum WHERE id = :id")
+        ComplexEntityWithEnum getComplexValueWithId(long id);
+    }
+
+    @Database(entities = {EntityWithEnum.class, ComplexEntityWithEnum.class}, version = 1,
+            exportSchema = false)
     public abstract static class EnumColumnTypeAdapterDatabase extends RoomDatabase {
         public abstract EnumColumnTypeAdapterTest.SampleDao dao();
+        public abstract EnumColumnTypeAdapterTest.SampleDaoWithComplexEnum complexDao();
     }
 
     @Before
@@ -77,6 +108,10 @@
                 context,
                 EnumColumnTypeAdapterDatabase.class)
                 .build();
+        mDbComplex = Room.inMemoryDatabaseBuilder(
+                context,
+                EnumColumnTypeAdapterDatabase.class)
+                .build();
     }
 
     @Test
@@ -87,4 +122,11 @@
         assertThat(mDb.dao().getValueWithId(1).fruit, is(equalTo(Fruit.BANANA)));
         assertThat(mDb.dao().getValueWithId(2).fruit, is(equalTo(Fruit.STRAWBERRY)));
     }
+
+    @Test
+    public void filterOutComplexEnumTest() {
+        final long id1 = mDbComplex.complexDao().insertComplex(1, Season.AUTUMN);
+        assertThat(mDbComplex.complexDao().getComplexValueWithId(1).mSeason,
+                is(equalTo(Season.AUTUMN)));
+    }
 }