Add initial composition translator, with tests, for Wear.

Relnote: n/a
Test: included (./gradlew :glance:glance-wear:test)
Change-Id: Ibe56089d8230f627ba4c7f3a64fb48273087f8b6
diff --git a/glance/glance-wear/api/current.txt b/glance/glance-wear/api/current.txt
index e6f50d0..93c7b08 100644
--- a/glance/glance-wear/api/current.txt
+++ b/glance/glance-wear/api/current.txt
@@ -1 +1,8 @@
 // Signature format: 4.0
+package androidx.glance.wear {
+
+  public final class WearCompositionTranslatorKt {
+  }
+
+}
+
diff --git a/glance/glance-wear/api/public_plus_experimental_current.txt b/glance/glance-wear/api/public_plus_experimental_current.txt
index e6f50d0..8544895 100644
--- a/glance/glance-wear/api/public_plus_experimental_current.txt
+++ b/glance/glance-wear/api/public_plus_experimental_current.txt
@@ -1 +1,9 @@
 // Signature format: 4.0
+package androidx.glance.wear {
+
+  public final class WearCompositionTranslatorKt {
+    method @androidx.glance.GlanceInternalApi public static androidx.wear.tiles.LayoutElementBuilders.LayoutElement translateComposition(androidx.glance.Emittable element);
+  }
+
+}
+
diff --git a/glance/glance-wear/api/restricted_current.txt b/glance/glance-wear/api/restricted_current.txt
index e6f50d0..93c7b08 100644
--- a/glance/glance-wear/api/restricted_current.txt
+++ b/glance/glance-wear/api/restricted_current.txt
@@ -1 +1,8 @@
 // Signature format: 4.0
+package androidx.glance.wear {
+
+  public final class WearCompositionTranslatorKt {
+  }
+
+}
+
diff --git a/glance/glance-wear/build.gradle b/glance/glance-wear/build.gradle
index c831070..335d2ea 100644
--- a/glance/glance-wear/build.gradle
+++ b/glance/glance-wear/build.gradle
@@ -33,18 +33,21 @@
     kotlinPlugin(project(":compose:compiler:compiler"))
 
     api(project(":glance:glance"))
-
+    api(project(":wear:tiles:tiles"))
+    
     implementation(libs.kotlinStdlib)
     implementation(project(":compose:runtime:runtime"))
 
     testImplementation(libs.testRules)
     testImplementation(libs.testRunner)
+    testImplementation(libs.truth)
     testImplementation(libs.junit)
+    testImplementation(libs.kotlinCoroutinesTest)
 }
 
 android {
     defaultConfig {
-        minSdkVersion 23
+        minSdkVersion 26
     }
     // Use Robolectric 4.+
     testOptions.unitTests.includeAndroidResources = true
diff --git a/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/WearCompositionTranslator.kt b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/WearCompositionTranslator.kt
new file mode 100644
index 0000000..e24f14c
--- /dev/null
+++ b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/WearCompositionTranslator.kt
@@ -0,0 +1,91 @@
+@file:OptIn(GlanceInternalApi::class)
+/*
+ * Copyright 2021 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.
+ * 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.
+ */
+
+package androidx.glance.wear
+
+import androidx.glance.Emittable
+import androidx.glance.GlanceInternalApi
+import androidx.glance.Modifier
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.EmittableBox
+import androidx.glance.layout.PaddingModifier
+import androidx.wear.tiles.DimensionBuilders.dp
+import androidx.wear.tiles.LayoutElementBuilders
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
+import androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignment
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_TOP
+import androidx.wear.tiles.LayoutElementBuilders.VerticalAlignment
+import androidx.wear.tiles.ModifiersBuilders
+import java.lang.IllegalArgumentException
+
+@VerticalAlignment private fun Alignment.Vertical.toProto(): Int =
+    when (this) {
+        Alignment.Vertical.Top -> VERTICAL_ALIGN_TOP
+        Alignment.Vertical.CenterVertically -> VERTICAL_ALIGN_CENTER
+        Alignment.Vertical.Bottom -> VERTICAL_ALIGN_BOTTOM
+        else -> throw IllegalArgumentException("Unknown vertical alignment type $this")
+    }
+
+@HorizontalAlignment private fun Alignment.Horizontal.toProto(): Int =
+    when (this) {
+        Alignment.Horizontal.Start -> HORIZONTAL_ALIGN_START
+        Alignment.Horizontal.CenterHorizontally -> HORIZONTAL_ALIGN_CENTER
+        Alignment.Horizontal.End -> HORIZONTAL_ALIGN_END
+        else -> throw IllegalArgumentException("Unknown horizontal alignment type $this")
+    }
+
+private fun PaddingModifier.toProto(): ModifiersBuilders.Padding =
+    ModifiersBuilders.Padding.Builder()
+        .setStart(dp(this.start.value))
+        .setTop(dp(this.top.value))
+        .setEnd(dp(this.end.value))
+        .setBottom(dp(this.bottom.value))
+        .setRtlAware(this.rtlAware)
+        .build()
+
+private fun translateEmittableBox(element: EmittableBox) = LayoutElementBuilders.Box.Builder()
+    .setVerticalAlignment(element.contentAlignment.vertical.toProto())
+    .setHorizontalAlignment(element.contentAlignment.horizontal.toProto())
+    .setModifiers(translateModifiers(element.modifier))
+    .also { box -> element.children.forEach { box.addContent(translateComposition(it)) } }
+    .build()
+
+private fun translateModifiers(modifier: Modifier): ModifiersBuilders.Modifiers = modifier
+    .foldOut(ModifiersBuilders.Modifiers.Builder()) { element, builder ->
+        when (element) {
+            is PaddingModifier -> builder.setPadding(element.toProto())
+            else -> throw IllegalArgumentException("Unknown modifier type")
+        }
+    }.build()
+
+/**
+ * Translates a Glance Composition to a Wear Tile.
+ *
+ * @throws IllegalArgumentException If the provided Emittable is not recognised (e.g. it is an
+ *   element which this translator doesn't understand).
+ */
+@GlanceInternalApi
+public fun translateComposition(element: Emittable): LayoutElementBuilders.LayoutElement {
+    return when (element) {
+        is EmittableBox -> translateEmittableBox(element)
+        else -> throw IllegalArgumentException("Unknown element $element")
+    }
+}
\ No newline at end of file
diff --git a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/Utils.kt b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/Utils.kt
new file mode 100644
index 0000000..857465b
--- /dev/null
+++ b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/Utils.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 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.
+ * 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.
+ */
+
+package androidx.glance.wear
+
+import androidx.compose.runtime.BroadcastFrameClock
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.Recomposer
+import androidx.glance.Applier
+import androidx.glance.GlanceInternalApi
+import androidx.glance.Modifier
+import androidx.glance.layout.EmittableBox
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.launch
+
+inline fun <reified T> Modifier.findModifier(): T? = this.foldOut<T?>(null) { cur, acc ->
+    if (cur is T) {
+        cur
+    } else {
+        acc
+    }
+}
+
+@OptIn(GlanceInternalApi::class)
+suspend fun runTestingComposition(content: @Composable () -> Unit): EmittableBox = coroutineScope {
+    val root = EmittableBox()
+    val applier = Applier(root)
+    val recomposer = Recomposer(currentCoroutineContext())
+    val composition = Composition(applier, recomposer)
+    val frameClock = BroadcastFrameClock()
+
+    composition.setContent { content() }
+
+    launch(frameClock) { recomposer.runRecomposeAndApplyChanges() }
+
+    recomposer.close()
+    recomposer.join()
+
+    root
+}
diff --git a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt
new file mode 100644
index 0000000..68e07bf
--- /dev/null
+++ b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2021 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.
+ * 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.
+ */
+
+package androidx.glance.wear
+
+import androidx.compose.runtime.Composable
+import androidx.glance.GlanceInternalApi
+import androidx.glance.Modifier
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.padding
+import androidx.glance.unit.dp
+import androidx.wear.tiles.LayoutElementBuilders
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_TOP
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(GlanceInternalApi::class, ExperimentalCoroutinesApi::class)
+class WearCompositionTranslatorTest {
+    private lateinit var fakeCoroutineScope: TestCoroutineScope
+
+    @Before
+    fun setUp() {
+        fakeCoroutineScope = TestCoroutineScope()
+    }
+
+    @Test
+    fun canTranslateBox() = fakeCoroutineScope.runBlockingTest {
+        val content = runAndTranslate {
+            Box {}
+        }
+
+        // runAndTranslate wraps the result in a Box...ensure that the layout generated two Boxes
+        val outerBox = content as LayoutElementBuilders.Box
+        assertThat(outerBox.contents).hasSize(1)
+
+        assertThat(outerBox.contents[0]).isInstanceOf(LayoutElementBuilders.Box::class.java)
+    }
+
+    @Test
+    fun canTranslateAlignment() = fakeCoroutineScope.runBlockingTest {
+        val content = runAndTranslate {
+            Box(contentAlignment = Alignment.Center) {}
+        }
+
+        val innerBox =
+            (content as LayoutElementBuilders.Box).contents[0] as LayoutElementBuilders.Box
+
+        assertThat(innerBox.verticalAlignment!!.value).isEqualTo(VERTICAL_ALIGN_CENTER)
+        assertThat(innerBox.horizontalAlignment!!.value).isEqualTo(HORIZONTAL_ALIGN_CENTER)
+    }
+
+    @Test
+    fun canTranslateBoxWithChildren() = fakeCoroutineScope.runBlockingTest {
+        val content = runAndTranslate {
+            Box {
+                Box(contentAlignment = Alignment.TopCenter) {}
+                Box(contentAlignment = Alignment.BottomEnd) {}
+            }
+        }
+
+        val innerBox =
+            (content as LayoutElementBuilders.Box).contents[0] as LayoutElementBuilders.Box
+        val leaf0 = innerBox.contents[0] as LayoutElementBuilders.Box
+        val leaf1 = innerBox.contents[1] as LayoutElementBuilders.Box
+
+        assertThat(leaf0.verticalAlignment!!.value).isEqualTo(VERTICAL_ALIGN_TOP)
+        assertThat(leaf0.horizontalAlignment!!.value).isEqualTo(HORIZONTAL_ALIGN_CENTER)
+
+        assertThat(leaf1.verticalAlignment!!.value).isEqualTo(VERTICAL_ALIGN_BOTTOM)
+        assertThat(leaf1.horizontalAlignment!!.value).isEqualTo(HORIZONTAL_ALIGN_END)
+    }
+
+    @Test
+    fun canTranslatePaddingModifier() = fakeCoroutineScope.runBlockingTest {
+        val content = runAndTranslate {
+            Box(modifier = Modifier.padding(start = 1.dp, top = 2.dp, end = 3.dp, bottom = 4.dp)) {}
+        }
+
+        val innerBox =
+            (content as LayoutElementBuilders.Box).contents[0] as LayoutElementBuilders.Box
+        val padding = requireNotNull(innerBox.modifiers!!.padding)
+
+        assertThat(padding.start!!.value).isEqualTo(1f)
+        assertThat(padding.top!!.value).isEqualTo(2f)
+        assertThat(padding.end!!.value).isEqualTo(3f)
+        assertThat(padding.bottom!!.value).isEqualTo(4f)
+    }
+
+    private suspend fun runAndTranslate(
+        content: @Composable () -> Unit
+    ): LayoutElementBuilders.LayoutElement {
+        val root = runTestingComposition(content)
+
+        return translateComposition(root)
+    }
+}
\ No newline at end of file