Support the `contentDescription` semantic modifier for glance-appWidgets. It already existed for glance-wear-tiles
Test: Manually tested using talkback
Change-Id: Iaf1d7099300c783fa1181315402d153466154ac0
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
index c94a102..51d1774 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
@@ -45,6 +45,8 @@
import androidx.glance.layout.HeightModifier
import androidx.glance.layout.PaddingModifier
import androidx.glance.layout.WidthModifier
+import androidx.glance.semantics.SemanticsModifier
+import androidx.glance.semantics.SemanticsProperties
import androidx.glance.unit.Dimension
import androidx.glance.unit.FixedColorProvider
import androidx.glance.unit.ResourceColorProvider
@@ -64,6 +66,7 @@
var actionModifier: ActionModifier? = null
var enabled: EnabledModifier? = null
var clipToOutline: ClipToOutlineModifier? = null
+ var semanticsModifier: SemanticsModifier? = null
modifiers.foldIn(Unit) { _, modifier ->
when (modifier) {
is ActionModifier -> {
@@ -100,6 +103,7 @@
}
is ClipToOutlineModifier -> clipToOutline = modifier
is EnabledModifier -> enabled = modifier
+ is SemanticsModifier -> semanticsModifier = modifier
else -> {
Log.w(GlanceAppWidgetTag, "Unknown modifier '$modifier', nothing done.")
}
@@ -127,6 +131,13 @@
enabled?.let {
rv.setBoolean(viewDef.mainViewId, "setEnabled", it.enabled)
}
+ semanticsModifier?.let { semantics ->
+ val contentDescription: List<String>? =
+ semantics.configuration.getOrNull(SemanticsProperties.ContentDescription)
+ if (contentDescription != null) {
+ rv.setContentDescription(viewDef.mainViewId, contentDescription.joinToString())
+ }
+ }
rv.setViewVisibility(viewDef.mainViewId, visibility.toViewVisibility())
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt
index 46c96e8..c3ddf74 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt
@@ -53,7 +53,6 @@
}
}
val viewDef = insertView(translationContext, selector, element.modifier)
- setContentDescription(viewDef.mainViewId, element.contentDescription)
when (val provider = element.provider) {
is AndroidResourceImageProvider -> setImageViewResource(
viewDef.mainViewId,
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
index 734a6eb..e84a322 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
@@ -21,6 +21,7 @@
import android.view.ViewGroup
import android.widget.ImageView
import androidx.compose.ui.graphics.Color
+import androidx.glance.GlanceModifier
import androidx.glance.appwidget.CheckBox
import androidx.glance.appwidget.checkBoxColors
import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
@@ -31,6 +32,8 @@
import androidx.glance.appwidget.findViewByType
import androidx.glance.appwidget.runAndTranslate
import androidx.glance.color.ColorProvider
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
import androidx.glance.unit.FixedColorProvider
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
@@ -221,4 +224,22 @@
val checkboxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
assertThat(checkboxRoot.hasOnClickListeners()).isTrue()
}
+
+ @Test
+ fun canTranslateCheckBoxWithSemanticsModifier_contentDescription() =
+ fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ CheckBox(
+ checked = true,
+ onCheckedChange = actionRunCallback<ActionCallback>(),
+ text = "CheckBox",
+ modifier = GlanceModifier.semantics {
+ contentDescription = "Custom checkbox description"
+ },
+ )
+ }
+
+ val checkboxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+ assertThat(checkboxRoot.contentDescription).isEqualTo("Custom checkbox description")
+ }
}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt
index 9236064..52bdc36 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt
@@ -25,6 +25,7 @@
import android.net.Uri
import android.widget.ImageView
import androidx.core.graphics.drawable.toBitmap
+import androidx.glance.GlanceModifier
import androidx.glance.appwidget.applyRemoteViews
import androidx.glance.appwidget.ImageProvider
import androidx.glance.appwidget.runAndTranslate
@@ -32,6 +33,8 @@
import androidx.glance.layout.ContentScale
import androidx.glance.Image
import androidx.glance.ImageProvider
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
@@ -172,4 +175,49 @@
assertThat(imageView.getContentDescription()).isEqualTo("oval")
assertThat(imageView.getScaleType()).isEqualTo(ImageView.ScaleType.FIT_XY)
}
+
+ @Test
+ fun translateImage_contentDescriptionFieldAndSemanticsSet_fieldPreferred() =
+ fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = "oval",
+ modifier = GlanceModifier.semantics { contentDescription = "round" },
+ )
+ }
+
+ val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+ assertThat(imageView.getContentDescription()).isEqualTo("oval")
+ }
+
+ @Test
+ fun translateImage_contentDescriptionFieldNullAndSemanticsSet_setFromSemantics() =
+ fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = null,
+ modifier = GlanceModifier.semantics { contentDescription = "round" },
+ )
+ }
+
+ val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+ assertThat(imageView.getContentDescription()).isEqualTo("round")
+ }
+
+ @Test
+ fun translateImage_contentDescriptionFieldAndSemanticsNull() =
+ fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = null,
+ modifier = GlanceModifier.semantics {},
+ )
+ }
+
+ val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+ assertThat(imageView.getContentDescription()).isNull()
+ }
}
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
index 1f249fc..892b5ed 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
@@ -22,6 +22,7 @@
import android.widget.ImageView
import android.widget.TextView
import androidx.compose.ui.graphics.Color
+import androidx.glance.GlanceModifier
import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
import androidx.glance.appwidget.RadioButton
import androidx.glance.appwidget.radioButtonColors
@@ -33,6 +34,8 @@
import androidx.glance.appwidget.findViewByType
import androidx.glance.appwidget.runAndTranslate
import androidx.glance.color.ColorProvider
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
import androidx.glance.unit.ColorProvider
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
@@ -265,6 +268,25 @@
assertThat(radioButtonRoot.hasOnClickListeners()).isFalse()
}
+ @Test
+ fun canTranslateRadioButtonWithSemanticsModifier_contentDescription() =
+ fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ RadioButton(
+ checked = true,
+ onClick = actionRunCallback<ActionCallback>(),
+ text = "RadioButton",
+ modifier = GlanceModifier.semantics {
+ contentDescription = "Custom radio button description"
+ },
+ )
+ }
+
+ val radioButtonRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+ assertThat(radioButtonRoot.contentDescription)
+ .isEqualTo("Custom radio button description")
+ }
+
private val ViewGroup.radioImageView: ImageView?
get() = findView {
shadowOf(it.drawable).createdFromResId ==
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
index 3b70ee7..9dce352 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
@@ -21,6 +21,7 @@
import android.view.ViewGroup
import android.widget.ImageView
import androidx.compose.ui.graphics.Color
+import androidx.glance.GlanceModifier
import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
import androidx.glance.appwidget.Switch
import androidx.glance.appwidget.action.ActionCallback
@@ -31,6 +32,8 @@
import androidx.glance.appwidget.runAndTranslate
import androidx.glance.appwidget.switchColors
import androidx.glance.color.ColorProvider
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
import androidx.glance.unit.ColorProvider
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
@@ -272,6 +275,23 @@
assertThat(switchRoot.hasOnClickListeners()).isTrue()
}
+ @Test
+ fun canTranslateSwitchWithSemanticsModifier_contentDescription() = fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Switch(
+ checked = true,
+ onCheckedChange = actionRunCallback<ActionCallback>(),
+ text = "Switch",
+ modifier = GlanceModifier.semantics {
+ contentDescription = "Custom switch description"
+ },
+ )
+ }
+
+ val switchRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
+ assertThat(switchRoot.contentDescription).isEqualTo("Custom switch description")
+ }
+
private val ViewGroup.thumbImageView: ImageView?
get() = findView {
shadowOf(it.drawable).createdFromResId ==
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
index 0d26478..b877f04 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
@@ -45,6 +45,8 @@
import androidx.glance.color.ColorProvider
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxWidth
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
import androidx.glance.text.FontStyle
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
@@ -371,6 +373,23 @@
assertThat(view.maxLines).isEqualTo(5)
}
+ @Test
+ fun canTranslateTextWithSemanticsModifier_contentDescription() = fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Text(
+ text = "Max line is set",
+ maxLines = 5,
+ modifier = GlanceModifier.semantics {
+ contentDescription = "Custom text description"
+ },
+ )
+ }
+ val view = context.applyRemoteViews(rv)
+
+ assertIs<TextView>(view)
+ assertThat(view.contentDescription).isEqualTo("Custom text description")
+ }
+
// Check there is a single span, that it's of the correct type and passes the [check].
private inline fun <reified T> SpannedString.checkSingleSpan(check: (T) -> Unit) {
val spans = getSpans(0, length, Any::class.java)
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
index 7e0ee8d..22b630f 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
@@ -561,7 +561,7 @@
val imageBuilder = LayoutElementBuilders.Image.Builder()
.setWidth(element.modifier.getWidth(context).toImageDimension())
.setHeight(element.modifier.getHeight(context).toImageDimension())
- .setModifiers(translateModifiers(context, element.modifier, element.contentDescription))
+ .setModifiers(translateModifiers(context, element.modifier))
.setResourceId(mappedResId)
.setContentScaleMode(
when (element.contentScale) {
@@ -701,7 +701,6 @@
private fun translateModifiers(
context: Context,
modifier: GlanceModifier,
- contentDescription: String? = null
): ModifiersBuilders.Modifiers =
modifier.foldIn(ModifiersBuilders.Modifiers.Builder()) { builder, element ->
when (element) {
@@ -727,14 +726,6 @@
?.let {
builder.setPadding(it.toProto())
}
-
- contentDescription?.let { contentDescription ->
- builder.setSemantics(
- ModifiersBuilders.Semantics.Builder()
- .setContentDescription(contentDescription)
- .build()
- )
- }
}
.build()
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
index 8b402b4..7869e12 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
@@ -24,6 +24,8 @@
import androidx.annotation.RestrictTo
import androidx.compose.runtime.Composable
import androidx.glance.layout.ContentScale
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
/**
* Interface representing an Image source which can be used with a Glance [Image] element.
@@ -78,20 +80,17 @@
override var modifier: GlanceModifier = GlanceModifier
var provider: ImageProvider? = null
- var contentDescription: String? = null
var contentScale: ContentScale = ContentScale.Fit
override fun copy(): Emittable = EmittableImage().also {
it.modifier = modifier
it.provider = provider
- it.contentDescription = contentDescription
it.contentScale = contentScale
}
override fun toString(): String = "EmittableImage(" +
"modifier=$modifier, " +
"provider=$provider, " +
- "contentDescription=$contentDescription, " +
"contentScale=$contentScale" +
")"
}
@@ -117,12 +116,19 @@
modifier: GlanceModifier = GlanceModifier,
contentScale: ContentScale = ContentScale.Fit
) {
+ val finalModifier = if (contentDescription != null) {
+ modifier.semantics {
+ this.contentDescription = contentDescription
+ }
+ } else {
+ modifier
+ }
+
GlanceNode(
factory = ::EmittableImage,
update = {
this.set(provider) { this.provider = it }
- this.set(contentDescription) { this.contentDescription = it }
- this.set(modifier) { this.modifier = it }
+ this.set(finalModifier) { this.modifier = it }
this.set(contentScale) { this.contentScale = it }
}
)
diff --git a/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt b/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
index 0a67b2e..38c08f1 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
@@ -21,6 +21,8 @@
import androidx.glance.layout.PaddingModifier
import androidx.glance.layout.padding
import androidx.glance.layout.runTestingComposition
+import androidx.glance.semantics.SemanticsModifier
+import androidx.glance.semantics.SemanticsProperties
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -28,6 +30,7 @@
import org.junit.Before
import org.junit.Test
import kotlin.test.assertIs
+import kotlin.test.assertNotNull
@OptIn(ExperimentalCoroutinesApi::class)
class ImageTest {
@@ -56,7 +59,9 @@
val imgSource = assertIs<AndroidResourceImageProvider>(img.provider)
assertThat(imgSource.resId).isEqualTo(5)
- assertThat(img.contentDescription).isEqualTo("Hello World")
+ val semanticsModifier = assertNotNull(img.modifier.findModifier<SemanticsModifier>())
+ assertThat(semanticsModifier.configuration[SemanticsProperties.ContentDescription])
+ .containsExactly("Hello World")
assertThat(img.contentScale).isEqualTo(ContentScale.FillBounds)
assertThat(img.modifier.findModifier<PaddingModifier>()).isNotNull()
}