[go: nahoru, domu]

Merge "[RangeSlider] Fix semantics when the thumbs don't have full range" into androidx-main
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
index 5646cae..8219742 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
@@ -366,7 +366,7 @@
         if (sCanApplyOverrideConfiguration
                 && baseContext instanceof android.view.ContextThemeWrapper) {
             final Configuration config = createOverrideConfigurationForDayNight(
-                    baseContext, modeToApply, null, false);
+                    baseContext, modeToApply, null);
             if (DEBUG) {
                 Log.d(TAG, String.format("Attempting to apply config to base context: %s",
                         config.toString()));
@@ -386,7 +386,7 @@
         // Again, but using the AppCompat version of ContextThemeWrapper.
         if (baseContext instanceof ContextThemeWrapper) {
             final Configuration config = createOverrideConfigurationForDayNight(
-                    baseContext, modeToApply, null, false);
+                    baseContext, modeToApply, null);
             if (DEBUG) {
                 Log.d(TAG, String.format("Attempting to apply config to base context: %s",
                         config.toString()));
@@ -443,7 +443,7 @@
         }
 
         final Configuration config = createOverrideConfigurationForDayNight(
-                baseContext, modeToApply, configOverlay, true);
+                baseContext, modeToApply, configOverlay);
         if (DEBUG) {
             Log.d(TAG, String.format("Applying night mode using ContextThemeWrapper and "
                     + "applyOverrideConfiguration(). Config: %s", config.toString()));
@@ -2464,7 +2464,7 @@
     @NonNull
     private Configuration createOverrideConfigurationForDayNight(
             @NonNull Context context, @ApplyableNightMode final int mode,
-            @Nullable Configuration configOverlay, boolean ignoreFollowSystem) {
+            @Nullable Configuration configOverlay) {
         int newNightMode;
         switch (mode) {
             case MODE_NIGHT_YES:
@@ -2475,15 +2475,11 @@
                 break;
             default:
             case MODE_NIGHT_FOLLOW_SYSTEM:
-                if (ignoreFollowSystem) {
-                    newNightMode = 0;
-                } else {
-                    // If we're following the system, we just use the system default from the
-                    // application context
-                    final Configuration appConfig =
-                            context.getApplicationContext().getResources().getConfiguration();
-                    newNightMode = appConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
-                }
+                // If we're following the system, we just use the system default from the
+                // application context
+                final Configuration appConfig =
+                        context.getApplicationContext().getResources().getConfiguration();
+                newNightMode = appConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
                 break;
         }
 
@@ -2512,7 +2508,7 @@
         boolean handled = false;
 
         final Configuration overrideConfig =
-                createOverrideConfigurationForDayNight(mContext, mode, null, false);
+                createOverrideConfigurationForDayNight(mContext, mode, null);
 
         final boolean activityHandlingUiMode = isActivityManifestHandlingUiMode(mContext);
         final Configuration currentConfiguration = mEffectiveConfiguration == null
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
index 333f99a..34c1e31 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
@@ -159,7 +159,7 @@
         Location location5 = new Location(SamplePlaces.class.getSimpleName());
         location5.setLatitude(37.422014);
         location5.setLongitude(-122.084776);
-        SpannableString title5 = new SpannableString(" ");
+        SpannableString title5 = new SpannableString("  Googleplex");
         title5.setSpan(CarIconSpan.create(new CarIcon.Builder(
                         IconCompat.createWithBitmap(
                                 BitmapFactory.decodeResource(
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
index 264ee00..6b5d0ad 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
@@ -160,8 +160,8 @@
          *
          * <p>Unless set with this method, the template will not have a title.
          *
-         * <p>Only {@link DistanceSpan}s and {@link DurationSpan}s are supported in the input
-         * string.
+         * <p>Only {@link DistanceSpan}s, {@link DurationSpan}s and {@link CarIconSpan} are
+         * supported in the input string.
          *
          * @throws NullPointerException     if {@code title} is {@code null}
          * @throws IllegalArgumentException if {@code title} contains unsupported spans
@@ -170,7 +170,7 @@
         @NonNull
         public Builder setTitle(@NonNull CharSequence title) {
             mTitle = CarText.create(requireNonNull(title));
-            CarTextConstraints.TEXT_ONLY.validateOrThrow(mTitle);
+            CarTextConstraints.TEXT_AND_ICON.validateOrThrow(mTitle);
             return this;
         }
 
@@ -237,7 +237,6 @@
          * set on the template, the header is hidden.
          *
          * @throws IllegalArgumentException if the {@link Pane} does not meet the requirements
-         *
          * @see androidx.car.app.constraints.ConstraintManager#getContentLimit(int)
          */
         @NonNull
diff --git a/car/app/app/src/test/java/androidx/car/app/model/PaneTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/PaneTemplateTest.java
index e52c422..1f07561 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/PaneTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PaneTemplateTest.java
@@ -46,6 +46,29 @@
     }
 
     @Test
+    public void paneTemplate_title_unsupportedSpans_throws() {
+        CharSequence title1 = TestUtils.getCharSequenceWithClickableSpan("Title");
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new PaneTemplate.Builder(TestUtils.createPane(2, 2)).setTitle(
+                        title1).build());
+
+        CharSequence title2 = TestUtils.getCharSequenceWithColorSpan("Title");
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new PaneTemplate.Builder(TestUtils.createPane(2, 2)).setTitle(
+                        title2).build());
+
+        // CarIconSpan assert no exceptions
+        CharSequence title3 = TestUtils.getCharSequenceWithIconSpan("Title");
+        new PaneTemplate.Builder(TestUtils.createPane(2, 2)).setTitle(title3).build();
+
+        // DistanceSpan and DurationSpan assert no exceptions
+        CharSequence title4 = TestUtils.getCharSequenceWithDistanceAndDurationSpans("Title");
+        new PaneTemplate.Builder(TestUtils.createPane(2, 2)).setTitle(title4).build();
+    }
+
+    @Test
     public void pane_action_unsupportedSpans_throws() {
         CharSequence title1 = TestUtils.getCharSequenceWithClickableSpan("Title");
         Action action1 = new Action.Builder().setTitle(title1).build();
@@ -97,8 +120,9 @@
     @Test
     public void pane_moreThanMaxPrimaryButtons_throws() {
         Action primaryAction = new Action.Builder().setTitle("primaryAction")
-                                       .setOnClickListener(() -> {})
-                                       .setFlags(FLAG_PRIMARY).build();
+                .setOnClickListener(() -> {
+                })
+                .setFlags(FLAG_PRIMARY).build();
         Row rowMeetingMaxTexts =
                 new Row.Builder().setTitle("Title").addText("text1").addText("text2").build();
 
@@ -112,8 +136,8 @@
         assertThrows(
                 IllegalArgumentException.class,
                 () -> new PaneTemplate.Builder(paneExceedsMaxPrimaryAction)
-                              .setTitle("Title")
-                              .build());
+                        .setTitle("Title")
+                        .build());
     }
 
     @Test
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
index 5782d58..8f0d760 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material3.tokens.NavigationBarTokens
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -59,7 +60,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import androidx.compose.material3.tokens.NavigationBarTokens
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -174,17 +174,20 @@
 
         rule.runOnIdleWithDensity {
             val totalWidth = parentCoords.size.width
+            val availableWidth =
+                totalWidth.toFloat() - (NavigationBarItemHorizontalPadding.toPx() * 3)
 
-            val expectedItemWidth = totalWidth / 4
-            val expectedItemHeight = NavigationBarTokens.ContainerHeight.roundToPx()
+            val expectedItemWidth = (availableWidth / 4)
+            val expectedItemHeight = NavigationBarTokens.ContainerHeight.toPx()
 
             Truth.assertThat(itemCoords.size).isEqualTo(4)
 
             itemCoords.forEach { (index, coord) ->
-                Truth.assertThat(coord.size.width).isEqualTo(expectedItemWidth)
-                Truth.assertThat(coord.size.height).isEqualTo(expectedItemHeight)
-                Truth.assertThat(coord.positionInWindow().x)
-                    .isEqualTo((expectedItemWidth * index).toFloat())
+                // Rounding differences for width can occur on smaller screens
+                Truth.assertThat(coord.size.width.toFloat()).isWithin(1f).of(expectedItemWidth)
+                Truth.assertThat(coord.size.height).isEqualTo(expectedItemHeight.toInt())
+                Truth.assertThat(coord.positionInWindow().x).isWithin(1f)
+                    .of((expectedItemWidth + NavigationBarItemHorizontalPadding.toPx()) * index)
             }
         }
     }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MappedInteractionSource.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MappedInteractionSource.kt
new file mode 100644
index 0000000..3004270
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MappedInteractionSource.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022 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.compose.material3
+
+import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.ui.geometry.Offset
+import kotlinx.coroutines.flow.map
+
+/**
+ * Adapts an [InteractionSource] from one component to another by mapping any interactions by a
+ * given offset. Namely used for the pill indicator in [NavigationBarItem] and [NavigationRailItem].
+ */
+internal class MappedInteractionSource(
+    underlyingInteractionSource: InteractionSource,
+    private val delta: Offset
+) : InteractionSource {
+    private val mappedPresses =
+        mutableMapOf<PressInteraction.Press, PressInteraction.Press>()
+
+    override val interactions = underlyingInteractionSource.interactions.map { interaction ->
+        when (interaction) {
+            is PressInteraction.Press -> {
+                val mappedPress = mapPress(interaction)
+                mappedPresses[interaction] = mappedPress
+                mappedPress
+            }
+            is PressInteraction.Cancel -> {
+                val mappedPress = mappedPresses.remove(interaction.press)
+                if (mappedPress == null) {
+                    interaction
+                } else {
+                    PressInteraction.Cancel(mappedPress)
+                }
+            }
+            is PressInteraction.Release -> {
+                val mappedPress = mappedPresses.remove(interaction.press)
+                if (mappedPress == null) {
+                    interaction
+                } else {
+                    PressInteraction.Release(mappedPress)
+                }
+            }
+            else -> interaction
+        }
+    }
+
+    private fun mapPress(press: PressInteraction.Press): PressInteraction.Press =
+        PressInteraction.Press(press.pressPosition - delta)
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
index 5fc7b6f..85fc603 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
@@ -20,6 +20,7 @@
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
+import androidx.compose.foundation.indication
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Arrangement
@@ -38,10 +39,14 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.LastBaseline
 import androidx.compose.ui.layout.Layout
@@ -49,6 +54,8 @@
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
@@ -99,7 +106,7 @@
     ) {
         Row(
             modifier = Modifier.fillMaxWidth().height(NavigationBarHeight).selectableGroup(),
-            horizontalArrangement = Arrangement.SpaceBetween,
+            horizontalArrangement = Arrangement.spacedBy(NavigationBarItemHorizontalPadding),
             content = content
         )
     }
@@ -166,6 +173,8 @@
         }
     }
 
+    var itemWidth by remember { mutableStateOf(0) }
+
     Box(
         modifier
             .selectable(
@@ -174,9 +183,12 @@
                 enabled = enabled,
                 role = Role.Tab,
                 interactionSource = interactionSource,
-                indication = rememberRipple(),
+                indication = null,
             )
-            .weight(1f),
+            .weight(1f)
+            .onSizeChanged {
+                itemWidth = it.width
+            },
         contentAlignment = Alignment.Center
     ) {
         val animationProgress: Float by animateFloatAsState(
@@ -184,6 +196,30 @@
             animationSpec = tween(ItemAnimationDurationMillis)
         )
 
+        // The entire item is selectable, but only the indicator pill shows the ripple. To achieve
+        // this, we re-map the coordinates of the item's InteractionSource into the coordinates of
+        // the indicator.
+        val deltaOffset: Offset
+        with(LocalDensity.current) {
+            val indicatorWidth = NavigationBarTokens.ActiveIndicatorWidth.roundToPx()
+            deltaOffset = Offset(
+                (itemWidth - indicatorWidth).toFloat() / 2,
+                IndicatorVerticalOffset.toPx()
+            )
+        }
+        val offsetInteractionSource = remember(interactionSource, deltaOffset) {
+            MappedInteractionSource(interactionSource, deltaOffset)
+        }
+
+        // The indicator has a width-expansion animation which interferes with the timing of the
+        // ripple, which is why they are separate composables
+        val indicatorRipple = @Composable {
+            Box(
+                Modifier.layoutId(IndicatorRippleLayoutIdTag)
+                    .clip(NavigationBarTokens.ActiveIndicatorShape.toShape())
+                    .indication(offsetInteractionSource, rememberRipple())
+            )
+        }
         val indicator = @Composable {
             Box(
                 Modifier.layoutId(IndicatorLayoutIdTag)
@@ -195,6 +231,7 @@
         }
 
         NavigationBarItemBaselineLayout(
+            indicatorRipple = indicatorRipple,
             indicator = indicator,
             icon = styledIcon,
             label = styledLabel,
@@ -297,6 +334,7 @@
 /**
  * Base layout for a [NavigationBarItem].
  *
+ * @param indicatorRipple indicator ripple for this item when it is selected
  * @param indicator indicator for this item when it is selected
  * @param icon icon for this item
  * @param label text label for this item
@@ -308,6 +346,7 @@
  */
 @Composable
 private fun NavigationBarItemBaselineLayout(
+    indicatorRipple: @Composable () -> Unit,
     indicator: @Composable () -> Unit,
     icon: @Composable () -> Unit,
     label: @Composable (() -> Unit)?,
@@ -315,6 +354,7 @@
     animationProgress: Float,
 ) {
     Layout({
+        indicatorRipple()
         if (animationProgress > 0) {
             indicator()
         }
@@ -325,7 +365,7 @@
             Box(
                 Modifier.layoutId(LabelLayoutIdTag)
                     .alpha(if (alwaysShowLabel) 1f else animationProgress)
-                    .padding(horizontal = NavigationBarItemHorizontalPadding)
+                    .padding(horizontal = NavigationBarItemHorizontalPadding / 2)
             ) { label() }
         }
     }) { measurables, constraints ->
@@ -335,6 +375,15 @@
         val totalIndicatorWidth = iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx()
         val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt()
         val indicatorHeight = iconPlaceable.height + (IndicatorVerticalPadding * 2).roundToPx()
+        val indicatorRipplePlaceable =
+            measurables
+                .first { it.layoutId == IndicatorRippleLayoutIdTag }
+                .measure(
+                    Constraints.fixed(
+                        width = totalIndicatorWidth,
+                        height = indicatorHeight
+                    )
+                )
         val indicatorPlaceable =
             measurables
                 .firstOrNull { it.layoutId == IndicatorLayoutIdTag }
@@ -357,11 +406,12 @@
             }
 
         if (label == null) {
-            placeIcon(iconPlaceable, indicatorPlaceable, constraints)
+            placeIcon(iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints)
         } else {
             placeLabelAndIcon(
                 labelPlaceable!!,
                 iconPlaceable,
+                indicatorRipplePlaceable,
                 indicatorPlaceable,
                 constraints,
                 alwaysShowLabel,
@@ -372,11 +422,11 @@
 }
 
 /**
- * Places the provided [iconPlaceable], and possibly [indicatorPlaceable] if it exists, in the
- * center of the provided [constraints].
+ * Places the provided [Placeable]s in the center of the provided [constraints].
  */
 private fun MeasureScope.placeIcon(
     iconPlaceable: Placeable,
+    indicatorRipplePlaceable: Placeable,
     indicatorPlaceable: Placeable?,
     constraints: Constraints
 ): MeasureResult {
@@ -386,6 +436,9 @@
     val iconX = (width - iconPlaceable.width) / 2
     val iconY = (height - iconPlaceable.height) / 2
 
+    val rippleX = (width - indicatorRipplePlaceable.width) / 2
+    val rippleY = (height - indicatorRipplePlaceable.height) / 2
+
     return layout(width, height) {
         indicatorPlaceable?.let {
             val indicatorX = (width - it.width) / 2
@@ -393,12 +446,13 @@
             it.placeRelative(indicatorX, indicatorY)
         }
         iconPlaceable.placeRelative(iconX, iconY)
+        indicatorRipplePlaceable.placeRelative(rippleX, rippleY)
     }
 }
 
 /**
- * Places the provided [labelPlaceable], [iconPlaceable], and [indicatorPlaceable] in the correct
- * position, depending on [alwaysShowLabel] and [animationProgress].
+ * Places the provided [Placeable]s in the correct position, depending on [alwaysShowLabel] and
+ * [animationProgress].
  *
  * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed
  * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to
@@ -413,11 +467,12 @@
  * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable]
  * will be placed at a corresponding interpolated position.
  *
- * [indicatorPlaceable] will always be placed in such a way that it shares the same center as
- * [iconPlaceable].
+ * [indicatorRipplePlaceable] and [indicatorPlaceable] will always be placed in such a way that to
+ * share the same center as [iconPlaceable].
  *
  * @param labelPlaceable text label placeable inside this item
  * @param iconPlaceable icon placeable inside this item
+ * @param indicatorRipplePlaceable indicator ripple placeable inside this item
  * @param indicatorPlaceable indicator placeable inside this item, if it exists
  * @param constraints constraints of the item
  * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label
@@ -430,6 +485,7 @@
 private fun MeasureScope.placeLabelAndIcon(
     labelPlaceable: Placeable,
     iconPlaceable: Placeable,
+    indicatorRipplePlaceable: Placeable,
     indicatorPlaceable: Placeable?,
     constraints: Constraints,
     alwaysShowLabel: Boolean,
@@ -458,6 +514,9 @@
     val labelX = (containerWidth - labelPlaceable.width) / 2
     val iconX = (containerWidth - iconPlaceable.width) / 2
 
+    val rippleX = (containerWidth - indicatorRipplePlaceable.width) / 2
+    val rippleY = selectedIconY - IndicatorVerticalPadding.roundToPx()
+
     return layout(containerWidth, height) {
         indicatorPlaceable?.let {
             val indicatorX = (containerWidth - it.width) / 2
@@ -468,9 +527,12 @@
             labelPlaceable.placeRelative(labelX, labelY + offset)
         }
         iconPlaceable.placeRelative(iconX, selectedIconY + offset)
+        indicatorRipplePlaceable.placeRelative(rippleX, rippleY + offset)
     }
 }
 
+private const val IndicatorRippleLayoutIdTag: String = "indicatorRipple"
+
 private const val IndicatorLayoutIdTag: String = "indicator"
 
 private const val IconLayoutIdTag: String = "icon"
@@ -481,7 +543,8 @@
 
 private const val ItemAnimationDurationMillis: Int = 100
 
-private val NavigationBarItemHorizontalPadding: Dp = 4.dp
+/*@VisibleForTesting*/
+internal val NavigationBarItemHorizontalPadding: Dp = 8.dp
 
 /*@VisibleForTesting*/
 internal val NavigationBarItemVerticalPadding: Dp = 16.dp
@@ -490,4 +553,6 @@
     (NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2
 
 private val IndicatorVerticalPadding: Dp =
-    (NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2
\ No newline at end of file
+    (NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2
+
+private val IndicatorVerticalOffset: Dp = 12.dp
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
index af24fb2..8616b23 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
@@ -20,6 +20,7 @@
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
+import androidx.compose.foundation.indication
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Arrangement
@@ -45,12 +46,15 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
@@ -179,7 +183,7 @@
                 enabled = enabled,
                 role = Role.Tab,
                 interactionSource = interactionSource,
-                indication = rememberRipple(),
+                indication = null,
             )
             .size(width = NavigationRailItemWidth, height = NavigationRailItemHeight),
         contentAlignment = Alignment.Center
@@ -189,21 +193,46 @@
             animationSpec = tween(ItemAnimationDurationMillis)
         )
 
+        // The entire item is selectable, but only the indicator pill shows the ripple. To achieve
+        // this, we re-map the coordinates of the item's InteractionSource into the coordinates of
+        // the indicator.
+        val deltaOffset: Offset
+        with(LocalDensity.current) {
+            val itemWidth = NavigationRailItemWidth.roundToPx()
+            val indicatorWidth = NavigationRailTokens.ActiveIndicatorWidth.roundToPx()
+            deltaOffset = Offset((itemWidth - indicatorWidth).toFloat() / 2, 0f)
+        }
+        val offsetInteractionSource = remember(interactionSource, deltaOffset) {
+            MappedInteractionSource(interactionSource, deltaOffset)
+        }
+
+        val indicatorShape = if (label != null) {
+            NavigationRailTokens.ActiveIndicatorShape.toShape()
+        } else {
+            NavigationRailTokens.NoLabelActiveIndicatorShape.toShape()
+        }
+
+        // The indicator has a width-expansion animation which interferes with the timing of the
+        // ripple, which is why they are separate composables
+        val indicatorRipple = @Composable {
+            Box(
+                Modifier.layoutId(IndicatorRippleLayoutIdTag)
+                    .clip(indicatorShape)
+                    .indication(offsetInteractionSource, rememberRipple())
+            )
+        }
         val indicator = @Composable {
             Box(
                 Modifier.layoutId(IndicatorLayoutIdTag)
                     .background(
                         color = colors.indicatorColor.copy(alpha = animationProgress),
-                        shape = if (label != null) {
-                            NavigationRailTokens.ActiveIndicatorShape.toShape()
-                        } else {
-                            NavigationRailTokens.NoLabelActiveIndicatorShape.toShape()
-                        }
+                        shape = indicatorShape
                     )
             )
         }
 
         NavigationRailItemBaselineLayout(
+            indicatorRipple = indicatorRipple,
             indicator = indicator,
             icon = styledIcon,
             label = styledLabel,
@@ -306,6 +335,7 @@
 /**
  * Base layout for a [NavigationRailItem].
  *
+ * @param indicatorRipple indicator ripple for this item when it is selected
  * @param indicator indicator for this item when it is selected
  * @param icon icon for this item
  * @param label text label for this item
@@ -317,6 +347,7 @@
  */
 @Composable
 private fun NavigationRailItemBaselineLayout(
+    indicatorRipple: @Composable () -> Unit,
     indicator: @Composable () -> Unit,
     icon: @Composable () -> Unit,
     label: @Composable (() -> Unit)?,
@@ -324,6 +355,7 @@
     animationProgress: Float,
 ) {
     Layout({
+        indicatorRipple()
         if (animationProgress > 0) {
             indicator()
         }
@@ -349,6 +381,15 @@
         }
         val indicatorHeight = iconPlaceable.height + (indicatorVerticalPadding * 2).roundToPx()
 
+        val indicatorRipplePlaceable =
+            measurables
+                .first { it.layoutId == IndicatorRippleLayoutIdTag }
+                .measure(
+                    Constraints.fixed(
+                        width = totalIndicatorWidth,
+                        height = indicatorHeight
+                    )
+                )
         val indicatorPlaceable =
             measurables
                 .firstOrNull { it.layoutId == IndicatorLayoutIdTag }
@@ -371,11 +412,12 @@
             }
 
         if (label == null) {
-            placeIcon(iconPlaceable, indicatorPlaceable, constraints)
+            placeIcon(iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints)
         } else {
             placeLabelAndIcon(
                 labelPlaceable!!,
                 iconPlaceable,
+                indicatorRipplePlaceable,
                 indicatorPlaceable,
                 constraints,
                 alwaysShowLabel,
@@ -386,11 +428,11 @@
 }
 
 /**
- * Places the provided [iconPlaceable], and possibly [indicatorPlaceable] if it exists, in the
- * center of the provided [constraints].
+ * Places the provided [Placeable]s in the center of the provided [constraints].
  */
 private fun MeasureScope.placeIcon(
     iconPlaceable: Placeable,
+    indicatorRipplePlaceable: Placeable,
     indicatorPlaceable: Placeable?,
     constraints: Constraints,
 ): MeasureResult {
@@ -400,6 +442,9 @@
     val iconX = (width - iconPlaceable.width) / 2
     val iconY = (height - iconPlaceable.height) / 2
 
+    val rippleX = (width - indicatorRipplePlaceable.width) / 2
+    val rippleY = (height - indicatorRipplePlaceable.height) / 2
+
     return layout(width, height) {
         indicatorPlaceable?.let {
             val indicatorX = (width - it.width) / 2
@@ -407,12 +452,13 @@
             it.placeRelative(indicatorX, indicatorY)
         }
         iconPlaceable.placeRelative(iconX, iconY)
+        indicatorRipplePlaceable.placeRelative(rippleX, rippleY)
     }
 }
 
 /**
- * Places the provided [labelPlaceable], [iconPlaceable], and [indicatorPlaceable] in the correct
- * position, depending on [alwaysShowLabel] and [animationProgress].
+ * Places the provided [Placeable]s in the correct position, depending on [alwaysShowLabel] and
+ * [animationProgress].
  *
  * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed
  * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to
@@ -427,11 +473,12 @@
  * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable]
  * will be placed at a corresponding interpolated position.
  *
- * [indicatorPlaceable] will always be placed in such a way that it shares the same center as
- * [iconPlaceable].
+ * [indicatorRipplePlaceable] and [indicatorPlaceable] will always be placed in such a way that to
+ * share the same center as [iconPlaceable].
  *
  * @param labelPlaceable text label placeable inside this item
  * @param iconPlaceable icon placeable inside this item
+ * @param indicatorRipplePlaceable indicator ripple placeable inside this item
  * @param indicatorPlaceable indicator placeable inside this item, if it exists
  * @param constraints constraints of the item
  * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label
@@ -444,6 +491,7 @@
 private fun MeasureScope.placeLabelAndIcon(
     labelPlaceable: Placeable,
     iconPlaceable: Placeable,
+    indicatorRipplePlaceable: Placeable,
     indicatorPlaceable: Placeable?,
     constraints: Constraints,
     alwaysShowLabel: Boolean,
@@ -469,6 +517,8 @@
     val width = constraints.maxWidth
     val labelX = (width - labelPlaceable.width) / 2
     val iconX = (width - iconPlaceable.width) / 2
+    val rippleX = (width - indicatorRipplePlaceable.width) / 2
+    val rippleY = selectedIconY - IndicatorVerticalPaddingWithLabel.roundToPx()
 
     return layout(width, height) {
         indicatorPlaceable?.let {
@@ -480,9 +530,12 @@
             labelPlaceable.placeRelative(labelX, labelY + offset)
         }
         iconPlaceable.placeRelative(iconX, selectedIconY + offset)
+        indicatorRipplePlaceable.placeRelative(rippleX, rippleY + offset)
     }
 }
 
+private const val IndicatorRippleLayoutIdTag: String = "indicatorRipple"
+
 private const val IndicatorLayoutIdTag: String = "indicator"
 
 private const val IconLayoutIdTag: String = "icon"
diff --git a/glance/glance-appwidget/build.gradle b/glance/glance-appwidget/build.gradle
index 3b13d9c..2b9509f 100644
--- a/glance/glance-appwidget/build.gradle
+++ b/glance/glance-appwidget/build.gradle
@@ -122,5 +122,6 @@
 LayoutGeneratorTask.registerLayoutGenerator(
         project,
         android,
-        /* layoutDirectory= */ file("src/androidMain/layoutTemplates")
+        /* containerLayoutDirectory= */ file("src/androidMain/layoutTemplates"),
+        /* childLayoutDirectory= */ file("src/androidMain/res/layout")
 )
diff --git a/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/GenerateRegistry.kt b/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/GenerateRegistry.kt
index 7f8735e..ccbb397 100644
--- a/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/GenerateRegistry.kt
+++ b/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/GenerateRegistry.kt
@@ -45,6 +45,8 @@
 internal fun generateRegistry(
     packageName: String,
     layouts: Map<File, List<ContainerProperties>>,
+    boxChildLayouts: Map<File, List<BoxChildProperties>>,
+    rowColumnChildLayouts: Map<File, List<RowColumnChildProperties>>,
     outputSourceDir: File,
 ) {
     outputSourceDir.mkdirs()
@@ -133,6 +135,24 @@
     file.addFunction(generatedChildrenApi21)
     file.addType(generatedContainerApi31)
 
+    // TODO: only register the box children on T+, since the layouts are in layout-v32
+    val generatedBoxChildren = propertySpec(
+        "generatedBoxChildren",
+        BoxChildrenMap,
+        INTERNAL,
+    ) {
+        initializer(buildBoxChildInitializer(boxChildLayouts))
+    }
+    file.addProperty(generatedBoxChildren)
+    val generatedRowColumnChildren = propertySpec(
+        "generatedRowColumnChildren",
+        RowColumnChildrenMap,
+        INTERNAL,
+    ) {
+        initializer(buildRowColumnChildInitializer(rowColumnChildLayouts))
+    }
+    file.addProperty(generatedRowColumnChildren)
+
     val generatedComplexLayouts = propertySpec("generatedComplexLayouts", LayoutsMap, INTERNAL) {
         initializer(buildComplexInitializer())
     }
@@ -201,6 +221,44 @@
     }
 }
 
+private fun buildBoxChildInitializer(layouts: Map<File, List<BoxChildProperties>>): CodeBlock =
+    buildCodeBlock {
+        withIndent {
+            addStatement("mapOf(")
+            withIndent {
+                add(
+                    layouts.map {
+                        it.key to createBoxChildFileInitializer(it.key, it.value)
+                    }
+                        .sortedBy { it.first.nameWithoutExtension }
+                        .map { it.second }
+                        .joinToCode("")
+                )
+            }
+            addStatement(")")
+        }
+    }
+
+private fun buildRowColumnChildInitializer(
+    layouts: Map<File, List<RowColumnChildProperties>>
+): CodeBlock =
+    buildCodeBlock {
+        withIndent {
+            addStatement("mapOf(")
+            withIndent {
+                add(
+                    layouts.map {
+                        it.key to createRowColumnChildFileInitializer(it.key, it.value)
+                    }
+                        .sortedBy { it.first.nameWithoutExtension }
+                        .map { it.second }
+                        .joinToCode("")
+                )
+            }
+            addStatement(")")
+        }
+    }
+
 private fun buildComplexInitializer(): CodeBlock {
     return buildCodeBlock {
         addStatement("mapOf(")
@@ -262,6 +320,42 @@
         }
     }
 
+private fun createBoxChildFileInitializer(
+    layout: File,
+    generated: List<BoxChildProperties>
+): CodeBlock =
+    buildCodeBlock {
+        val viewType = layout.nameWithoutExtension.toLayoutType()
+        generated.forEach { props ->
+            addBoxChild(
+                resourceName = makeBoxChildResourceName(
+                    layout,
+                    props.horizontalAlignment,
+                    props.verticalAlignment
+                ),
+                viewType = viewType,
+                horizontalAlignment = props.horizontalAlignment,
+                verticalAlignment = props.verticalAlignment,
+            )
+        }
+    }
+
+private fun createRowColumnChildFileInitializer(
+    layout: File,
+    generated: List<RowColumnChildProperties>
+): CodeBlock =
+    buildCodeBlock {
+        val viewType = layout.nameWithoutExtension.toLayoutType()
+        generated.forEach { props ->
+            addRowColumnChild(
+                resourceName = makeRowColumnChildResourceName(layout, props.width, props.height),
+                viewType = viewType,
+                width = props.width,
+                height = props.height,
+            )
+        }
+    }
+
 private fun createChildrenInitializer(
     layout: File,
     generated: List<ContainerProperties>,
@@ -350,8 +444,41 @@
     addStatement(") to %T(layoutId = R.layout.$resourceName),", ContainerInfo)
 }
 
+private fun CodeBlock.Builder.addBoxChild(
+    resourceName: String,
+    viewType: String,
+    horizontalAlignment: HorizontalAlignment,
+    verticalAlignment: VerticalAlignment,
+) {
+    addStatement("%T(", BoxChildSelector)
+    withIndent {
+        addStatement("type = %M,", makeViewType(viewType))
+        addStatement("horizontalAlignment = %M, ", horizontalAlignment.code)
+        addStatement("verticalAlignment = %M, ", verticalAlignment.code)
+    }
+    addStatement(") to %T(layoutId = R.layout.$resourceName),", LayoutInfo)
+}
+
+private fun CodeBlock.Builder.addRowColumnChild(
+    resourceName: String,
+    viewType: String,
+    width: ValidSize,
+    height: ValidSize,
+) {
+    addStatement("%T(", RowColumnChildSelector)
+    withIndent {
+        addStatement("type = %M,", makeViewType(viewType))
+        addStatement("expandWidth = %L, ", width == ValidSize.Expand)
+        addStatement("expandHeight = %L, ", height == ValidSize.Expand)
+    }
+    addStatement(") to %T(layoutId = R.layout.$resourceName),", LayoutInfo)
+}
+
 private val ContainerSelector = ClassName("androidx.glance.appwidget", "ContainerSelector")
 private val SizeSelector = ClassName("androidx.glance.appwidget", "SizeSelector")
+private val BoxChildSelector = ClassName("androidx.glance.appwidget", "BoxChildSelector")
+private val RowColumnChildSelector =
+    ClassName("androidx.glance.appwidget", "RowColumnChildSelector")
 private val LayoutInfo = ClassName("androidx.glance.appwidget", "LayoutInfo")
 private val ContainerInfo = ClassName("androidx.glance.appwidget", "ContainerInfo")
 private val ContainerMap = Map::class.asTypeName().parameterizedBy(ContainerSelector, ContainerInfo)
@@ -381,6 +508,9 @@
 private val LayoutType = ClassName("androidx.glance.appwidget", "LayoutType")
 private val ChildrenMap = Map::class.asTypeName().parameterizedBy(INT, SizeSelectorToIntMap)
 private val ContainerChildrenMap = Map::class.asTypeName().parameterizedBy(LayoutType, ChildrenMap)
+private val BoxChildrenMap = Map::class.asTypeName().parameterizedBy(BoxChildSelector, LayoutInfo)
+private val RowColumnChildrenMap =
+    Map::class.asTypeName().parameterizedBy(RowColumnChildSelector, LayoutInfo)
 
 private fun makeViewType(name: String) =
     MemberName("androidx.glance.appwidget.LayoutType", name)
@@ -443,6 +573,28 @@
         pos
     ).joinToString(separator = "_")
 
+internal fun makeBoxChildResourceName(
+    file: File,
+    horizontalAlignment: HorizontalAlignment?,
+    verticalAlignment: VerticalAlignment?
+) =
+    listOf(
+        file.nameWithoutExtension,
+        horizontalAlignment?.resourceName,
+        verticalAlignment?.resourceName,
+    ).joinToString(separator = "_")
+
+internal fun makeRowColumnChildResourceName(
+    file: File,
+    width: ValidSize,
+    height: ValidSize,
+) =
+    listOf(
+        file.nameWithoutExtension,
+        if (width == ValidSize.Expand) "expandwidth" else "wrapwidth",
+        if (height == ValidSize.Expand) "expandheight" else "wrapheight",
+    ).joinToString(separator = "_")
+
 internal fun makeIdName(pos: Int, width: ValidSize, height: ValidSize) =
     listOf(
         "childStub$pos",
diff --git a/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/LayoutGenerator.kt b/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/LayoutGenerator.kt
index 74ce99f..480095c4 100644
--- a/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/LayoutGenerator.kt
+++ b/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/LayoutGenerator.kt
@@ -74,14 +74,17 @@
      * information extracted from the input.
      */
     fun generateAllFiles(
-        files: List<File>,
+        containerFiles: List<File>,
+        childrenFiles: List<File>,
         outputResourcesDir: File
     ): GeneratedFiles {
         val outputLayoutDir = outputResourcesDir.resolve("layout")
         val outputLayoutDirS = outputResourcesDir.resolve("layout-v31")
+        val outputLayoutDirT = outputResourcesDir.resolve("layout-v33")
         val outputValueDir = outputResourcesDir.resolve("values")
         outputLayoutDir.mkdirs()
         outputLayoutDirS.mkdirs()
+        outputLayoutDirT.mkdirs()
         outputValueDir.mkdirs()
         val generatedFiles = generateSizeLayouts(outputLayoutDir) +
             generateComplexLayouts(outputLayoutDir) +
@@ -90,10 +93,17 @@
             generateContainersChildrenBeforeS(outputLayoutDir) +
             generateRootElements(outputLayoutDir) +
             generateRootAliases(outputValueDir)
+        val topLevelLayouts = containerFiles + childrenFiles.filter { isTopLevelLayout(it) }
         return GeneratedFiles(
-            generatedContainers = files.associateWith {
+            generatedContainers = containerFiles.associateWith {
                 generateContainers(it, outputLayoutDir)
             },
+            generatedBoxChildren = topLevelLayouts.associateWith {
+                generateBoxChildrenForT(it, outputLayoutDirT)
+            },
+            generatedRowColumnChildren = topLevelLayouts.associateWith {
+                generateRowColumnChildrenForT(it, outputLayoutDirT)
+            },
             extraFiles = generatedFiles,
         )
     }
@@ -413,6 +423,42 @@
         }
     }
 
+    private fun generateBoxChildrenForT(
+        file: File,
+        outputLayoutDir: File,
+    ): List<BoxChildProperties> =
+        crossProduct(
+            HorizontalAlignment.values().toList(),
+            VerticalAlignment.values().toList()
+        ).map { (horizontalAlignment, verticalAlignment) ->
+            val generated = generateAlignedLayout(
+                parseLayoutTemplate(file),
+                horizontalAlignment,
+                verticalAlignment,
+            )
+            val output = outputLayoutDir.resolveRes(
+                makeBoxChildResourceName(file, horizontalAlignment, verticalAlignment)
+            )
+            writeGeneratedLayout(generated, output)
+            BoxChildProperties(output, horizontalAlignment, verticalAlignment)
+        }
+
+    private fun generateRowColumnChildrenForT(
+        file: File,
+        outputLayoutDir: File,
+    ): List<RowColumnChildProperties> =
+        listOf(
+            Pair(ValidSize.Expand, ValidSize.Wrap),
+            Pair(ValidSize.Wrap, ValidSize.Expand),
+        ).map { (width, height) ->
+            val generated = generateSimpleLayout(parseLayoutTemplate(file), width, height)
+            val output = outputLayoutDir.resolveRes(
+                makeRowColumnChildResourceName(file, width, height)
+            )
+            writeGeneratedLayout(generated, output)
+            RowColumnChildProperties(output, width, height)
+        }
+
     /**
      * Generate a simple layout.
      *
@@ -442,6 +488,25 @@
         return generated
     }
 
+    /**
+     * This function is used to generate FrameLayout children with "layout_gravity" set for
+     * Android T+. We can ignore size here since it is set programmatically for T+.
+     */
+    private fun generateAlignedLayout(
+        document: Document,
+        horizontalAlignment: HorizontalAlignment,
+        verticalAlignment: VerticalAlignment,
+    ) = generateSimpleLayout(document, ValidSize.Wrap, ValidSize.Wrap).apply {
+        documentElement.attributes.setNamedItemNS(
+            androidGravity(
+                listOfNotNull(
+                    horizontalAlignment.resourceName,
+                    verticalAlignment.resourceName
+                ).joinToString(separator = "|")
+            )
+        )
+    }
+
     private fun generateRootAliases(outputValueDir: File) =
         generateRes(outputValueDir, "layouts") {
             val root = createElement("resources")
@@ -474,6 +539,11 @@
         writeGeneratedLayout(document, file)
         return file
     }
+
+    private fun isTopLevelLayout(file: File) =
+        parseLayoutTemplate(file).run {
+            documentElement.appAttr("glance_isTopLevelLayout")?.nodeValue == "true"
+        }
 }
 
 /** Maximum number of children generated in containers. */
@@ -487,6 +557,8 @@
 
 internal data class GeneratedFiles(
     val generatedContainers: Map<File, List<ContainerProperties>>,
+    val generatedBoxChildren: Map<File, List<BoxChildProperties>>,
+    val generatedRowColumnChildren: Map<File, List<RowColumnChildProperties>>,
     val extraFiles: Set<File>
 )
 
@@ -504,6 +576,18 @@
     val verticalAlignment: VerticalAlignment?,
 )
 
+internal data class BoxChildProperties(
+    val generatedFile: File,
+    val horizontalAlignment: HorizontalAlignment,
+    val verticalAlignment: VerticalAlignment,
+)
+
+internal data class RowColumnChildProperties(
+    val generatedFile: File,
+    val width: ValidSize,
+    val height: ValidSize,
+)
+
 internal enum class ValidSize(val androidValue: String, val resourceName: String) {
     Wrap("wrap_content", "wrap"),
     Fixed("wrap_content", "fixed"),
@@ -557,6 +641,7 @@
 internal fun getChildMergeFilenameWithoutExtension(childCount: Int) = "merge_${childCount}child"
 
 private val AndroidNS = "http://schemas.android.com/apk/res/android"
+private val AppNS = "http://schemas.android.com/apk/res-auto"
 
 internal fun Document.androidAttr(name: String, value: String) =
     createAttributeNS(AndroidNS, "android:$name").apply {
@@ -566,6 +651,9 @@
 internal fun Node.androidAttr(name: String): Node? =
     attributes.getNamedItemNS(AndroidNS, name)
 
+internal fun Node.appAttr(name: String): Node? =
+    attributes.getNamedItemNS(AppNS, name)
+
 internal fun Document.attribute(name: String, value: String): Node? =
     createAttribute(name).apply { textContent = value }
 
diff --git a/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/gradle/LayoutGeneratorTask.kt b/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/gradle/LayoutGeneratorTask.kt
index 85212e9..fbe5c31 100644
--- a/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/gradle/LayoutGeneratorTask.kt
+++ b/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/gradle/LayoutGeneratorTask.kt
@@ -16,7 +16,7 @@
 
 package androidx.glance.appwidget.layoutgenerator.gradle
 
-import androidx.glance.appwidget.layoutgenerator.ContainerProperties
+import androidx.glance.appwidget.layoutgenerator.GeneratedFiles
 import androidx.glance.appwidget.layoutgenerator.LayoutGenerator
 import androidx.glance.appwidget.layoutgenerator.cleanResources
 import androidx.glance.appwidget.layoutgenerator.generateRegistry
@@ -43,7 +43,11 @@
 
     @get:PathSensitive(PathSensitivity.NAME_ONLY)
     @get:InputDirectory
-    abstract val layoutDirectory: DirectoryProperty
+    abstract val containerLayoutDirectory: DirectoryProperty
+
+    @get:PathSensitive(PathSensitivity.NAME_ONLY)
+    @get:InputDirectory
+    abstract val childLayoutDirectory: DirectoryProperty
 
     @get:OutputDirectory
     abstract val outputSourceDir: DirectoryProperty
@@ -53,27 +57,35 @@
 
     @TaskAction
     fun execute() {
-        val generatedFiles = LayoutGenerator().generateAllFiles(
-            checkNotNull(layoutDirectory.get().asFile.listFiles()).asList(),
+        val generatedLayouts = LayoutGenerator().generateAllFiles(
+            checkNotNull(containerLayoutDirectory.get().asFile.listFiles()).asList(),
+            checkNotNull(childLayoutDirectory.get().asFile.listFiles()).asList(),
             outputResourcesDir.get().asFile
         )
         generateRegistry(
             packageName = outputModule,
-            layouts = generatedFiles.generatedContainers,
+            layouts = generatedLayouts.generatedContainers,
+            boxChildLayouts = generatedLayouts.generatedBoxChildren,
+            rowColumnChildLayouts = generatedLayouts.generatedRowColumnChildren,
             outputSourceDir = outputSourceDir.get().asFile
         )
-        val generatedContainers = generatedFiles.generatedContainers.extractGeneratedFiles()
         cleanResources(
             outputResourcesDir.get().asFile,
-            generatedContainers +
-                generatedFiles.extraFiles
+            generatedLayouts.extractGeneratedFiles()
         )
     }
 
-    private fun Map<File, List<ContainerProperties>>.extractGeneratedFiles(): Set<File> =
-        values.flatMap { containers ->
-            containers.map { it.generatedFile }
-        }.toSet()
+    private fun GeneratedFiles.extractGeneratedFiles(): Set<File> =
+        generatedContainers.values.flatMap { container ->
+            container.map { it.generatedFile }
+        }.toSet() +
+        generatedBoxChildren.values.flatMap { child ->
+            child.map { it.generatedFile }
+        }.toSet() +
+        generatedRowColumnChildren.values.flatMap { child ->
+            child.map { it.generatedFile }
+        }.toSet() +
+        extraFiles
 
     companion object {
         /**
@@ -83,7 +95,8 @@
         fun registerLayoutGenerator(
             project: Project,
             libraryExtension: LibraryExtension,
-            layoutDirectory: File
+            containerLayoutDirectory: File,
+            childLayoutDirectory: File,
         ) {
             libraryExtension.libraryVariants.all { variant ->
                 val variantName = variant.name
@@ -95,7 +108,8 @@
                 outputResourcesDir.mkdirs()
                 outputSourceDir.mkdirs()
                 val task = project.tasks.register(taskName, LayoutGeneratorTask::class.java) {
-                    it.layoutDirectory.set(layoutDirectory)
+                    it.containerLayoutDirectory.set(containerLayoutDirectory)
+                    it.childLayoutDirectory.set(childLayoutDirectory)
                     it.outputResourcesDir.set(outputResourcesDir)
                     it.outputSourceDir.set(outputSourceDir)
                 }
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AlignmentModifier.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AlignmentModifier.kt
new file mode 100644
index 0000000..304c783
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AlignmentModifier.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.appwidget
+
+import androidx.glance.GlanceModifier
+import androidx.glance.layout.Alignment
+
+internal class AlignmentModifier(val alignment: Alignment) : GlanceModifier.Element
\ No newline at end of file
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 33a27b7..c089b81 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
@@ -94,6 +94,9 @@
                     )
                 }
             }
+            is AlignmentModifier -> {
+                // This modifier is handled somewhere else.
+            }
             else -> {
                 Log.w(GlanceAppWidgetTag, "Unknown modifier '$modifier', nothing done.")
             }
@@ -200,8 +203,9 @@
             "Using a width of $width requires a complex layout before API 31"
         )
     }
-    // Wrap and Expand are done in XML on Android S+
-    if (width in listOf(Dimension.Wrap, Dimension.Expand)) return
+    // Wrap and Expand are done in XML on Android S & Sv2
+    if (Build.VERSION.SDK_INT < 33 &&
+        width in listOf(Dimension.Wrap, Dimension.Expand)) return
     ApplyModifiersApi31Impl.setViewWidth(rv, viewId, width)
 }
 
@@ -229,8 +233,9 @@
             "Using a height of $height requires a complex layout before API 31"
         )
     }
-    // Wrap and Expand are done in XML on Android S+
-    if (height in listOf(Dimension.Wrap, Dimension.Expand)) return
+    // Wrap and Expand are done in XML on Android S & Sv2
+    if (Build.VERSION.SDK_INT < 33 &&
+        height in listOf(Dimension.Wrap, Dimension.Expand)) return
     ApplyModifiersApi31Impl.setViewHeight(rv, viewId, height)
 }
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
index 80e1045..0ba075d 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -199,11 +199,15 @@
         state: Any?,
         options: Bundle
     ): RemoteViews {
-        val layoutConfig = LayoutConfiguration.load(context, appWidgetId)
+        val layoutConfig = if (Build.VERSION.SDK_INT >= 33) {
+            null
+        } else {
+            LayoutConfiguration.load(context, appWidgetId)
+        }
         return try {
             compose(context, appWidgetManager, appWidgetId, state, options, layoutConfig)
         } finally {
-            layoutConfig.save()
+            layoutConfig?.save()
         }
     }
 
@@ -214,7 +218,7 @@
         appWidgetId: Int,
         state: Any?,
         options: Bundle,
-        layoutConfig: LayoutConfiguration,
+        layoutConfig: LayoutConfiguration?,
     ): RemoteViews =
         when (val localSizeMode = this.sizeMode) {
             is SizeMode.Single -> {
@@ -289,7 +293,7 @@
         appWidgetId: Int,
         state: Any?,
         options: Bundle,
-        layoutConfig: LayoutConfiguration,
+        layoutConfig: LayoutConfiguration?,
     ) = coroutineScope {
         val views =
             options.extractOrientationSizes()
@@ -331,7 +335,7 @@
         state: Any?,
         options: Bundle,
         sizes: Set<DpSize>,
-        layoutConfig: LayoutConfiguration,
+        layoutConfig: LayoutConfiguration?,
     ) = coroutineScope {
         // Find the best view, emulating what Android S+ would do.
         val orderedSizes = sizes.sortedBySize()
@@ -370,7 +374,7 @@
         state: Any?,
         options: Bundle,
         size: DpSize,
-        layoutConfig: LayoutConfiguration,
+        layoutConfig: LayoutConfiguration?,
     ): RemoteViews = withContext(BroadcastFrameClock()) {
         // The maximum depth must be reduced if the compositions are combined
         val root = RemoteViewsRoot(maxDepth = MaxComposeTreeDepth)
@@ -398,7 +402,7 @@
             appWidgetId,
             root,
             layoutConfig,
-            layoutConfig.addLayout(root),
+            layoutConfig?.addLayout(root) ?: 0,
             size
         )
     }
@@ -421,7 +425,7 @@
             state: Any?,
             options: Bundle,
             allSizes: Collection<DpSize>,
-            layoutConfig: LayoutConfiguration
+            layoutConfig: LayoutConfiguration?
         ): RemoteViews = coroutineScope {
             val allViews =
                 allSizes.map { size ->
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
index ef8564ac..7a3c5fd 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
@@ -20,8 +20,10 @@
 import android.view.View
 import android.view.ViewGroup
 import android.widget.RemoteViews
+import androidx.annotation.DoNotInline
 import androidx.annotation.IdRes
 import androidx.annotation.LayoutRes
+import androidx.annotation.RequiresApi
 import androidx.compose.ui.unit.dp
 import androidx.glance.GlanceModifier
 import androidx.glance.findModifier
@@ -68,6 +70,29 @@
 
 internal data class ContainerInfo(@LayoutRes val layoutId: Int)
 
+/**
+ * Box child selector.
+ *
+ * This class is used to select a layout with a particular alignment to be used as a child of
+ * Box.
+ */
+internal data class BoxChildSelector(
+    val type: LayoutType,
+    val horizontalAlignment: Alignment.Horizontal,
+    val verticalAlignment: Alignment.Vertical,
+)
+
+/**
+ * Selector for children of [Row] and [Column].
+ *
+ * This class is used to select a layout with layout_weight set / unset.
+ */
+internal data class RowColumnChildSelector(
+    val type: LayoutType,
+    val expandWidth: Boolean,
+    val expandHeight: Boolean,
+)
+
 /** Type of size needed for a layout. */
 internal enum class LayoutSize {
     Wrap,
@@ -167,6 +192,20 @@
     aliasIndex: Int
 ): RemoteViewsInfo {
     val context = translationContext.context
+    if (Build.VERSION.SDK_INT >= 33) {
+        return RemoteViewsInfo(
+            remoteViews = remoteViews(translationContext, FirstRootAlias).apply {
+                modifier.findModifier<WidthModifier>()?.let {
+                    applySimpleWidthModifier(context, this, it, R.id.rootView)
+                }
+                modifier.findModifier<HeightModifier>()?.let {
+                    applySimpleHeightModifier(context, this, it, R.id.rootView)
+                }
+                removeAllViews(R.id.rootView)
+            },
+            view = InsertedViewInfo(mainViewId = R.id.rootView)
+        )
+    }
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
         require(aliasIndex < RootAliasCount) {
             "Index of the root view cannot be more than $RootAliasCount, " +
@@ -186,7 +225,10 @@
                     applySimpleHeightModifier(context, this, it, R.id.rootView)
                 }
             },
-            view = InsertedViewInfo(children = mapOf(0 to mapOf(sizeSelector to R.id.rootStubId)))
+            view = InsertedViewInfo(
+                mainViewId = R.id.rootView,
+                children = mapOf(0 to mapOf(sizeSelector to R.id.rootStubId)),
+            )
         )
     }
     require(RootAliasTypeCount * aliasIndex < RootAliasCount) {
@@ -209,12 +251,45 @@
     )
 }
 
+@IdRes
+private fun selectLayout33(
+    type: LayoutType,
+    modifier: GlanceModifier,
+): Int? {
+    if (Build.VERSION.SDK_INT < 33) return null
+    val align = modifier.findModifier<AlignmentModifier>()
+    val expandWidth =
+        modifier.findModifier<WidthModifier>()?.let { it.width == Dimension.Expand } ?: false
+    val expandHeight =
+        modifier.findModifier<HeightModifier>()?.let { it.height == Dimension.Expand } ?: false
+    if (align != null) {
+        return generatedBoxChildren[BoxChildSelector(
+            type,
+            align.alignment.horizontal,
+            align.alignment.vertical,
+        )]?.layoutId
+            ?: throw IllegalArgumentException(
+                "Cannot find $type with alignment ${align.alignment}"
+            )
+    } else if (expandWidth || expandHeight) {
+        return generatedRowColumnChildren[RowColumnChildSelector(
+            type,
+            expandWidth,
+            expandHeight,
+        )]?.layoutId
+            ?: throw IllegalArgumentException("Cannot find $type with defaultWeight set")
+    } else {
+        return null
+    }
+}
+
 internal fun RemoteViews.insertView(
     translationContext: TranslationContext,
     type: LayoutType,
     modifier: GlanceModifier
 ): InsertedViewInfo {
-    val childLayout = LayoutMap[type]
+    val childLayout = selectLayout33(type, modifier)
+        ?: LayoutMap[type]
         ?: throw IllegalArgumentException("Cannot use `insertView` with a container like $type")
     return insertViewInternal(translationContext, childLayout, modifier)
 }
@@ -236,6 +311,16 @@
         }
         android.R.id.background
     }
+    if (Build.VERSION.SDK_INT >= 33) {
+        val viewId = specifiedViewId ?: translationContext.nextViewId()
+        val child = LayoutSelectionApi31Impl.remoteViews(
+            translationContext.context.packageName,
+            childLayout,
+            viewId,
+        )
+        addChildView(translationContext.parentContext.mainViewId, child, pos)
+        return InsertedViewInfo(mainViewId = viewId)
+    }
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
         val width = if (widthMod == Dimension.Expand) LayoutSize.Expand else LayoutSize.Wrap
         val height = if (heightMod == Dimension.Expand) LayoutSize.Expand else LayoutSize.Wrap
@@ -292,16 +377,17 @@
     horizontalAlignment: Alignment.Horizontal?,
     verticalAlignment: Alignment.Vertical?,
 ): InsertedViewInfo {
-    val childLayout = generatedContainers[ContainerSelector(
-        type,
-        numChildren,
-        horizontalAlignment,
-        verticalAlignment
-    )]
+    val childLayout = selectLayout33(type, modifier)
+        ?: generatedContainers[ContainerSelector(
+            type,
+            if (Build.VERSION.SDK_INT >= 33) 0 else numChildren,
+            horizontalAlignment,
+            verticalAlignment
+        )]?.layoutId
         ?: throw IllegalArgumentException("Cannot find container $type with $numChildren children")
     val childrenMapping = generatedChildren[type]
         ?: throw IllegalArgumentException("Cannot find generated children for $type")
-    return insertViewInternal(translationContext, childLayout.layoutId, modifier)
+    return insertViewInternal(translationContext, childLayout, modifier)
         .copy(children = childrenMapping)
 }
 
@@ -322,3 +408,13 @@
         else -> Dimension.Dp((sizePx / context.resources.displayMetrics.density).dp)
     }
 }
+
+@RequiresApi(Build.VERSION_CODES.S)
+private object LayoutSelectionApi31Impl {
+    @DoNotInline
+    fun remoteViews(
+        packageName: String,
+        @LayoutRes layoutId: Int,
+        viewId: Int
+    ) = RemoteViews(packageName, layoutId, viewId)
+}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
index 13c021f..f0e6933 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
@@ -60,7 +60,7 @@
     context: Context,
     appWidgetId: Int,
     element: RemoteViewsRoot,
-    layoutConfiguration: LayoutConfiguration,
+    layoutConfiguration: LayoutConfiguration?,
     rootViewIndex: Int,
     layoutSize: DpSize,
 ) =
@@ -104,7 +104,7 @@
     val context: Context,
     val appWidgetId: Int,
     val isRtl: Boolean,
-    val layoutConfiguration: LayoutConfiguration,
+    val layoutConfiguration: LayoutConfiguration?,
     val itemPosition: Int,
     val isLazyCollectionDescendant: Boolean = false,
     val lastViewId: AtomicInteger = AtomicInteger(0),
@@ -228,6 +228,9 @@
         element.modifier,
         viewDef
     )
+    element.children.forEach {
+        it.modifier = it.modifier.then(AlignmentModifier(element.contentAlignment))
+    }
     setChildren(
         translationContext,
         viewDef,
@@ -256,7 +259,7 @@
     )
     setLinearLayoutGravity(
         viewDef.mainViewId,
-        element.horizontalAlignment.toGravity()
+        Alignment(element.horizontalAlignment, element.verticalAlignment).toGravity()
     )
     applyModifiers(
         translationContext.canUseSelectableGroup(),
@@ -293,7 +296,7 @@
     )
     setLinearLayoutGravity(
         viewDef.mainViewId,
-        element.verticalAlignment.toGravity()
+        Alignment(element.horizontalAlignment, element.verticalAlignment).toGravity()
     )
     applyModifiers(
         translationContext.canUseSelectableGroup(),
@@ -386,7 +389,7 @@
 /**
  * Add stable view if on Android S+, otherwise simply add the view.
  */
-private fun RemoteViews.addChildView(viewId: Int, childView: RemoteViews, stableId: Int) {
+internal fun RemoteViews.addChildView(viewId: Int, childView: RemoteViews, stableId: Int) {
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
         RemoteViewsTranslatorApi31Impl.addChildView(this, viewId, childView, stableId)
         return
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
index 5e0f9c4..5dd29af 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
@@ -79,7 +79,7 @@
                 translateComposition(
                     childContext.forLazyViewItem(position, LazyListItemStartingViewId),
                     listOf(itemEmittable),
-                    translationContext.layoutConfiguration.addLayout(itemEmittable),
+                    translationContext.layoutConfiguration?.addLayout(itemEmittable) ?: -1,
                 )
             )
             // If the user specifies any explicit ids, we assume the list to be stable
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
index 7d727b0..84b359c 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
@@ -89,7 +89,7 @@
                 translateComposition(
                     childContext.forLazyViewItem(position, LazyVerticalGridItemStartingViewId),
                     listOf(itemEmittable),
-                    translationContext.layoutConfiguration.addLayout(itemEmittable),
+                    translationContext.layoutConfiguration?.addLayout(itemEmittable) ?: -1,
                 )
             )
             // If the user specifies any explicit ids, we assume the list to be stable
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_button.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_button.xml
index 7594c00..9489e43 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_button.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_button.xml
@@ -15,6 +15,8 @@
   -->
 
 <Button xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     style="@style/Glance.AppWidget.Button"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_check_box.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_check_box.xml
index af3d736..c23e6f4 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_check_box.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_check_box.xml
@@ -15,6 +15,8 @@
   -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:textDirection="locale"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_check_box_backport.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_check_box_backport.xml
index 3df83c2..b9d053c 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_check_box_backport.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_check_box_backport.xml
@@ -15,6 +15,8 @@
   -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:tag="glanceCompoundButton"
     style="@style/Glance.AppWidget.CheckBoxBackport"
     android:layout_width="wrap_content"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_circular_progress_indicator.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_circular_progress_indicator.xml
index 0bb1dae2d..5cd903d 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_circular_progress_indicator.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_circular_progress_indicator.xml
@@ -15,6 +15,8 @@
   -->
 
 <ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     style="@style/Glance.AppWidget.CircularProgressIndicator"/>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_frame.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_frame.xml
index adfa3c0..37347b6 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_frame.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_frame.xml
@@ -15,5 +15,7 @@
   -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="match_parent"
     android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_image_crop.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_image_crop.xml
index 0e0b278..d635cb6 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_image_crop.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_image_crop.xml
@@ -15,6 +15,8 @@
   -->
 
 <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:scaleType="centerCrop" />
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_image_fill_bounds.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_image_fill_bounds.xml
index 107dd70..786ab91 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_image_fill_bounds.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_image_fill_bounds.xml
@@ -15,6 +15,8 @@
   -->
 
 <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:scaleType="fitXY" />
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_image_fit.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_image_fit.xml
index 8f7b4ba..97740bb 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_image_fit.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_image_fit.xml
@@ -15,6 +15,8 @@
   -->
 
 <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:scaleType="fitCenter" />
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_linear_progress_indicator.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_linear_progress_indicator.xml
index 918ac5c..775ca38 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_linear_progress_indicator.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_linear_progress_indicator.xml
@@ -15,6 +15,8 @@
   -->
 
 <ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     style="@style/Glance.AppWidget.LinearProgressIndicator"/>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_list.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_list.xml
index 9758a8a..fe0fa2d 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_list.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_list.xml
@@ -15,6 +15,8 @@
   -->
 
 <ListView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:divider="@null"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_radio_button.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_radio_button.xml
index 6a3cbd9..718a865 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_radio_button.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_radio_button.xml
@@ -15,6 +15,8 @@
   -->
 
 <RadioButton xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:tag="glanceCompoundButton"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_radio_button_backport.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_radio_button_backport.xml
index 1ca712d..3499fec 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_radio_button_backport.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_radio_button_backport.xml
@@ -15,6 +15,8 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:tag="glanceCompoundButton"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_swtch.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_swtch.xml
index c5780e5..931c1fb 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_swtch.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_swtch.xml
@@ -15,6 +15,8 @@
   -->
 
 <Switch xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:tag="glanceCompoundButton"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_swtch_backport.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_swtch_backport.xml
index 6c34960..ac71093 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_swtch_backport.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_swtch_backport.xml
@@ -15,6 +15,8 @@
   -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:tag="glanceCompoundButton"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_text.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_text.xml
index d04f6ac..04cb0ea 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_text.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_text.xml
@@ -15,6 +15,8 @@
   -->
 
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:textDirection="locale"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_auto_fit.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_auto_fit.xml
index bc9f7e45..090f44c0 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_auto_fit.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_auto_fit.xml
@@ -15,6 +15,8 @@
   -->
 
 <GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:divider="@null"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_five_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_five_columns.xml
index d4eb3d0..55d7461 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_five_columns.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_five_columns.xml
@@ -15,6 +15,8 @@
   -->
 
 <GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:divider="@null"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_four_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_four_columns.xml
index c5779a6..662fdd4 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_four_columns.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_four_columns.xml
@@ -15,6 +15,8 @@
   -->
 
 <GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:divider="@null"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_one_column.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_one_column.xml
index 130b429..9d2b48f0 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_one_column.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_one_column.xml
@@ -15,6 +15,8 @@
   -->
 
 <GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:divider="@null"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_three_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_three_columns.xml
index 226862ec..a8ef7e9 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_three_columns.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_three_columns.xml
@@ -15,6 +15,8 @@
   -->
 
 <GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:divider="@null"
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_two_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_two_columns.xml
index 5243b80..73b745d 100644
--- a/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_two_columns.xml
+++ b/glance/glance-appwidget/src/androidMain/res/layout/glance_vertical_grid_two_columns.xml
@@ -15,6 +15,8 @@
   -->
 
 <GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:glance_isTopLevelLayout="true"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:divider="@null"
diff --git a/glance/glance-appwidget/src/androidMain/res/values/attrs.xml b/glance/glance-appwidget/src/androidMain/res/values/attrs.xml
new file mode 100644
index 0000000..82db37a
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/values/attrs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="GlanceAppWidget">
+	    <attr name="glance_isTopLevelLayout" format="boolean" />
+    </declare-styleable>
+</resources>
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallOnCast.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallOnCast.java
new file mode 100644
index 0000000..c3d7f33
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallOnCast.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 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;
+
+import android.os.Build;
+import android.view.DisplayCutout;
+
+/**
+ * Test class containing unsafe reference on cast object.
+ */
+@SuppressWarnings("unused")
+public class AutofixUnsafeCallOnCast {
+    /**
+     * Method making unsafe reference on cast object.
+     */
+    public void unsafeReferenceOnCastObject(Object secretDisplayCutout) {
+        if (Build.VERSION.SDK_INT >= 28) {
+            ((DisplayCutout) secretDisplayCutout).getSafeInsetTop();
+        }
+    }
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallToThis.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallToThis.java
index 3ea59db..6697751 100644
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallToThis.java
+++ b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallToThis.java
@@ -39,4 +39,22 @@
             getClipToPadding();
         }
     }
+
+    /**
+     * Method making the unsafe reference on an explicit this.
+     */
+    public void unsafeReferenceOnExplicitThis() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            this.getClipToPadding();
+        }
+    }
+
+    /**
+     * Method making the unsafe reference on an explicit super.
+     */
+    public void unsafeReferenceOnExplicitSuper() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            super.getClipToPadding();
+        }
+    }
 }
diff --git a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
index 9ca404b..56316d6 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
@@ -61,8 +61,6 @@
 import org.jetbrains.uast.UThisExpression
 import org.jetbrains.uast.getContainingUClass
 import org.jetbrains.uast.getContainingUMethod
-import org.jetbrains.uast.java.JavaUQualifiedReferenceExpression
-import org.jetbrains.uast.java.JavaUSimpleNameReferenceExpression
 import org.jetbrains.uast.util.isConstructorCall
 import org.jetbrains.uast.util.isMethodCall
 
@@ -522,7 +520,7 @@
                 call.valueArguments,
                 wrapperClassName,
                 wrapperMethodName
-            ) ?: return null
+            )
 
             return fix().name("Extract to static inner class")
                 .composite(
@@ -602,9 +600,7 @@
         }
 
         /**
-         * Generates source code for a call to the generated wrapper method, or `null` if we don't
-         * know how to do that. Currently, this method is capable of handling static calls --
-         * including constructor calls -- and simple reference expressions from Java source code.
+         * Generates source code for a call to the generated wrapper method.
          *
          * Source code follows the general format:
          *
@@ -625,12 +621,7 @@
             callValueArguments: List<UExpression>,
             wrapperClassName: String,
             wrapperMethodName: String,
-        ): String? {
-            var unwrappedCallReceiver = callReceiver
-            while (unwrappedCallReceiver is UParenthesizedExpression) {
-                unwrappedCallReceiver = unwrappedCallReceiver.expression
-            }
-
+        ): String {
             val callReceiverStr = when {
                 // Static method
                 context.evaluator.isStatic(method) ->
@@ -640,19 +631,11 @@
                     null
                 // If there is no call receiver, and the method isn't a constructor or static,
                 // it must be a call to an instance method using `this` implicitly.
-                unwrappedCallReceiver == null ->
+                callReceiver == null ->
                     "this"
-                // Simple reference
-                unwrappedCallReceiver is JavaUSimpleNameReferenceExpression ->
-                    unwrappedCallReceiver.identifier
-                // Qualified reference
-                unwrappedCallReceiver is JavaUQualifiedReferenceExpression ->
-                    "${unwrappedCallReceiver.receiver}.${unwrappedCallReceiver.selector}"
-                else -> {
-                    // We don't know how to handle this type of receiver. If this happens a lot, we
-                    // might try returning `UElement.asSourceString()` by default.
-                    return null
-                }
+                // Otherwise, use the original call receiver string (removing extra parens)
+                else ->
+                    unwrapExpression(callReceiver).asSourceString()
             }
 
             val callValues = if (callValueArguments.isNotEmpty()) {
@@ -669,6 +652,18 @@
         }
 
         /**
+         * Remove parentheses from the expression (unwrap the expression until it is no longer a
+         * UParenthesizedExpression).
+         */
+        private fun unwrapExpression(expr: UExpression): UExpression {
+            var unwrappedExpr = expr
+            while (unwrappedExpr is UParenthesizedExpression) {
+                unwrappedExpr = unwrappedExpr.expression
+            }
+            return unwrappedExpr
+        }
+
+        /**
          * Generates source code for a wrapper method, or `null` if we don't know how to do that.
          * Currently, this method is capable of handling method and constructor calls from Java
          * source code.
diff --git a/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
index d4527ff..aa02f01 100644
--- a/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
@@ -458,7 +458,13 @@
 src/androidx/AutofixUnsafeCallToThis.java:39: Error: This call references a method added in API level 21; however, the containing class androidx.AutofixUnsafeCallToThis is reachable from earlier API levels and will fail run-time class verification. [ClassVerificationFailure]
             getClipToPadding();
             ~~~~~~~~~~~~~~~~
-1 errors, 0 warnings
+src/androidx/AutofixUnsafeCallToThis.java:48: Error: This call references a method added in API level 21; however, the containing class androidx.AutofixUnsafeCallToThis is reachable from earlier API levels and will fail run-time class verification. [ClassVerificationFailure]
+            this.getClipToPadding();
+                 ~~~~~~~~~~~~~~~~
+src/androidx/AutofixUnsafeCallToThis.java:57: Error: This call references a method added in API level 21; however, the containing class androidx.AutofixUnsafeCallToThis is reachable from earlier API levels and will fail run-time class verification. [ClassVerificationFailure]
+            super.getClipToPadding();
+                  ~~~~~~~~~~~~~~~~
+3 errors, 0 warnings
         """
 
         val expectedFix = """
@@ -466,7 +472,7 @@
 @@ -39 +39
 -             getClipToPadding();
 +             Api21Impl.getClipToPadding(this);
-@@ -42 +42
+@@ -60 +60
 + @annotation.RequiresApi(21)
 + static class Api21Impl {
 +     private Api21Impl() {
@@ -478,7 +484,82 @@
 +         return viewGroup.getClipToPadding();
 +     }
 +
-@@ -43 +54
+@@ -61 +72
++ }
+Fix for src/androidx/AutofixUnsafeCallToThis.java line 48: Extract to static inner class:
+@@ -48 +48
+-             this.getClipToPadding();
++             Api21Impl.getClipToPadding(this);
+@@ -60 +60
++ @annotation.RequiresApi(21)
++ static class Api21Impl {
++     private Api21Impl() {
++         // This class is not instantiable.
++     }
++
++     @annotation.DoNotInline
++     static boolean getClipToPadding(ViewGroup viewGroup) {
++         return viewGroup.getClipToPadding();
++     }
++
+@@ -61 +72
++ }
+Fix for src/androidx/AutofixUnsafeCallToThis.java line 57: Extract to static inner class:
+@@ -57 +57
+-             super.getClipToPadding();
++             Api21Impl.getClipToPadding(super);
+@@ -60 +60
++ @annotation.RequiresApi(21)
++ static class Api21Impl {
++     private Api21Impl() {
++         // This class is not instantiable.
++     }
++
++     @annotation.DoNotInline
++     static boolean getClipToPadding(ViewGroup viewGroup) {
++         return viewGroup.getClipToPadding();
++     }
++
+@@ -61 +72
++ }
+        """
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected).expectFixDiffs(expectedFix)
+    }
+
+    @Test
+    fun `Auto-fix for unsafe method call on cast object (issue 206111383)`() {
+        val input = arrayOf(
+            javaSample("androidx.AutofixUnsafeCallOnCast")
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/androidx/AutofixUnsafeCallOnCast.java:32: Error: This call references a method added in API level 28; however, the containing class androidx.AutofixUnsafeCallOnCast is reachable from earlier API levels and will fail run-time class verification. [ClassVerificationFailure]
+            ((DisplayCutout) secretDisplayCutout).getSafeInsetTop();
+                                                  ~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+        """
+
+        val expectedFix = """
+Fix for src/androidx/AutofixUnsafeCallOnCast.java line 32: Extract to static inner class:
+@@ -32 +32
+-             ((DisplayCutout) secretDisplayCutout).getSafeInsetTop();
++             Api28Impl.getSafeInsetTop((DisplayCutout) secretDisplayCutout);
+@@ -35 +35
++ @annotation.RequiresApi(28)
++ static class Api28Impl {
++     private Api28Impl() {
++         // This class is not instantiable.
++     }
++
++     @annotation.DoNotInline
++     static int getSafeInsetTop(DisplayCutout displayCutout) {
++         return displayCutout.getSafeInsetTop();
++     }
++
+@@ -36 +47
 + }
         """
         /* ktlint-enable max-line-length */
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
new file mode 100644
index 0000000..0ca27652
--- /dev/null
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022 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.test.uiautomator.testapp;
+
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public abstract class BaseTest {
+
+    private static final long TIMEOUT_MS = 10_000;
+    protected static final String TEST_APP = "androidx.test.uiautomator.testapp";
+
+    protected UiDevice mDevice;
+
+    @Before
+    public void setUp() {
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    @After
+    public void tearDown() {
+        mDevice.pressHome();
+        assertTrue("Test app still visible after teardown",
+                mDevice.wait(Until.gone(By.pkg(TEST_APP)), TIMEOUT_MS));
+    }
+
+    protected void launchTestActivity(@NonNull Class<? extends Activity> activity) {
+        Context context = ApplicationProvider.getApplicationContext();
+        context.startActivity(new Intent().setClass(context, activity)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK));
+        assertTrue("Test app not visible after launching activity",
+                mDevice.wait(Until.hasObject(By.pkg(TEST_APP)), TIMEOUT_MS));
+    }
+}
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTests.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTests.java
index d8c1cd8..ae677dd 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTests.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTests.java
@@ -16,8 +16,11 @@
 
 package androidx.test.uiautomator.testapp;
 
-import android.content.Context;
-import android.content.Intent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
 import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.EditText;
@@ -29,253 +32,216 @@
 import android.widget.TextView;
 import android.widget.ToggleButton;
 
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
-import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.UiObject2;
-import androidx.test.uiautomator.Until;
 
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 import java.util.regex.Pattern;
 
-@RunWith(AndroidJUnit4.class)
-public class BySelectorTests {
-
-    private static final String TAG = BySelectorTests.class.getSimpleName();
-
-    private static final String TEST_APP = "androidx.test.uiautomator.testapp";
-    private static final String ANDROID_WIDGET_PACKAGE = "android.widget";
-
-    private UiDevice mDevice;
-
-    @Before
-    public void setUp() throws Exception {
-        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-    }
-
-    public void launchTestActivity(String activity) {
-        // Launch the test app
-        Context context = ApplicationProvider.getApplicationContext();
-        Intent intent = new Intent()
-                .setClassName(TEST_APP, String.format("%s.%s", TEST_APP, activity))
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-        context.startActivity(intent);
-
-        // Wait for activity to appear
-        mDevice.wait(Until.hasObject(By.pkg(TEST_APP)), 10000);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        mDevice.pressHome();
-
-        // Wait for the activity to disappear
-        mDevice.wait(Until.gone(By.pkg(TEST_APP)), 5000);
-    }
+public class BySelectorTests extends BaseTest {
 
     @Test
     public void testCopy() {
-        launchTestActivity("MainActivity");
+        launchTestActivity(MainActivity.class);
 
         // Base selector
         BySelector base = By.clazz(".TextView");
 
         // Select various TextView instances
-        Assert.assertNotNull(mDevice.findObject(By.copy(base).text("Text View 1")));
-        Assert.assertNotNull(mDevice.findObject(By.copy(base).text("Item1")));
-        Assert.assertNotNull(mDevice.findObject(By.copy(base).text("Item3")));
+        assertNotNull(mDevice.findObject(By.copy(base).text("Text View 1")));
+        assertNotNull(mDevice.findObject(By.copy(base).text("Item1")));
+        assertNotNull(mDevice.findObject(By.copy(base).text("Item3")));
 
         // Shouldn't be able to select an object that does not match the base
-        Assert.assertNull(mDevice.findObject(By.copy(base).text("Accessible button")));
+        assertNull(mDevice.findObject(By.copy(base).text("Accessible button")));
     }
 
     @Test
     public void testClazzButton() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
         // Button
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget", "Button")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget.Button")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(".Button")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(Button.class)));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget", "Button")));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget.Button")));
+        assertNotNull(mDevice.findObject(By.clazz(".Button")));
+        assertNotNull(mDevice.findObject(By.clazz(Button.class)));
     }
 
     @Test
     public void testClazzCheckBox() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
         // CheckBox
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget", "CheckBox")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget.CheckBox")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(".CheckBox")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(CheckBox.class)));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget", "CheckBox")));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget.CheckBox")));
+        assertNotNull(mDevice.findObject(By.clazz(".CheckBox")));
+        assertNotNull(mDevice.findObject(By.clazz(CheckBox.class)));
     }
 
     @Test
     public void testClazzEditText() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
         // EditText
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget", "EditText")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget.EditText")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(".EditText")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(EditText.class)));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget", "EditText")));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget.EditText")));
+        assertNotNull(mDevice.findObject(By.clazz(".EditText")));
+        assertNotNull(mDevice.findObject(By.clazz(EditText.class)));
     }
 
     @Test
     public void testClazzProgressBar() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
         // ProgressBar
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget", "ProgressBar")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget.ProgressBar")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(".ProgressBar")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(ProgressBar.class)));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget", "ProgressBar")));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget.ProgressBar")));
+        assertNotNull(mDevice.findObject(By.clazz(".ProgressBar")));
+        assertNotNull(mDevice.findObject(By.clazz(ProgressBar.class)));
     }
 
     @Test
     public void testClazzRadioButton() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
         // RadioButton
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget", "RadioButton")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget.RadioButton")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(".RadioButton")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(RadioButton.class)));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget", "RadioButton")));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget.RadioButton")));
+        assertNotNull(mDevice.findObject(By.clazz(".RadioButton")));
+        assertNotNull(mDevice.findObject(By.clazz(RadioButton.class)));
     }
 
     @Test
     public void testClazzRatingBar() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
         // RatingBar
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget", "RatingBar")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget.RatingBar")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(".RatingBar")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(RatingBar.class)));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget", "RatingBar")));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget.RatingBar")));
+        assertNotNull(mDevice.findObject(By.clazz(".RatingBar")));
+        assertNotNull(mDevice.findObject(By.clazz(RatingBar.class)));
     }
 
     @Test
     public void testClazzSeekBar() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
         // SeekBar
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget", "SeekBar")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget.SeekBar")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(".SeekBar")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(SeekBar.class)));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget", "SeekBar")));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget.SeekBar")));
+        assertNotNull(mDevice.findObject(By.clazz(".SeekBar")));
+        assertNotNull(mDevice.findObject(By.clazz(SeekBar.class)));
     }
 
     @Test
     public void testClazzSwitch() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
         // Switch
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget", "Switch")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget.Switch")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(".Switch")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(Switch.class)));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget", "Switch")));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget.Switch")));
+        assertNotNull(mDevice.findObject(By.clazz(".Switch")));
+        assertNotNull(mDevice.findObject(By.clazz(Switch.class)));
     }
 
     @Test
     public void testClazzTextView() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
         // TextView
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget", "TextView")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget.TextView")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(".TextView")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(TextView.class)));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget", "TextView")));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget.TextView")));
+        assertNotNull(mDevice.findObject(By.clazz(".TextView")));
+        assertNotNull(mDevice.findObject(By.clazz(TextView.class)));
     }
 
     @Test
     public void testClazzToggleButton() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
         // ToggleButton
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget", "ToggleButton")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz("android.widget.ToggleButton")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(".ToggleButton")));
-        Assert.assertNotNull(mDevice.findObject(By.clazz(ToggleButton.class)));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget", "ToggleButton")));
+        assertNotNull(mDevice.findObject(By.clazz("android.widget.ToggleButton")));
+        assertNotNull(mDevice.findObject(By.clazz(".ToggleButton")));
+        assertNotNull(mDevice.findObject(By.clazz(ToggleButton.class)));
     }
 
     @Test
     public void testClazzNotFound() {
-        launchTestActivity("BySelectorTestClazzActivity");
+        launchTestActivity(BySelectorTestClazzActivity.class);
 
-        // Non-existant class
-        Assert.assertNull(mDevice.findObject(By.clazz("android.widget", "NonExistantClass")));
-        Assert.assertNull(mDevice.findObject(By.clazz("android.widget.NonExistantClass")));
-        Assert.assertNull(mDevice.findObject(By.clazz(".NonExistantClass")));
+        // Non-existent class
+        assertNull(mDevice.findObject(By.clazz("android.widget", "NonExistentClass")));
+        assertNull(mDevice.findObject(By.clazz("android.widget.NonExistentClass")));
+        assertNull(mDevice.findObject(By.clazz(".NonExistentClass")));
     }
 
     @Test
     public void testClazzNull() {
         // clazz(String)
         try {
-            mDevice.findObject(By.clazz((String)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.clazz((String) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
 
         // clazz(String, String)
         try {
-            mDevice.findObject(By.clazz((String)null, "foo"));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.clazz((String) null, "foo"));
+            fail();
+        } catch (NullPointerException expected) {
+        }
 
         try {
-            mDevice.findObject(By.clazz("foo", (String)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.clazz("foo", (String) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
 
         // clazz(Class)
         try {
-            mDevice.findObject(By.clazz((Class)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.clazz((Class) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
 
         // clazz(Pattern)
         try {
-            mDevice.findObject(By.clazz((Pattern)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.clazz((Pattern) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
     }
 
-    // TODO(allenhair): Implement these for clazz():
+    // TODO(b/235841286): Implement these for clazz():
     // 1. Custom class
     // 2. Patterns
     // 3. Runtime Widgets
 
     @Test
     public void testDescSetFromResource() {
-        launchTestActivity("BySelectorTestDescActivity");
+        launchTestActivity(BySelectorTestDescActivity.class);
 
         // Content Description from resource
-        Assert.assertNotNull(mDevice.findObject(By.desc("Content Description Set From Layout")));
+        assertNotNull(mDevice.findObject(By.desc("Content Description Set From Layout")));
     }
 
     @Test
     public void testDescSetAtRuntime() {
-        launchTestActivity("BySelectorTestDescActivity");
+        launchTestActivity(BySelectorTestDescActivity.class);
 
         // Content Description set at runtime
-        Assert.assertNotNull(mDevice.findObject(By.desc("Content Description Set At Runtime")));
+        assertNotNull(mDevice.findObject(By.desc("Content Description Set At Runtime")));
     }
 
     @Test
     public void testDescNotFound() {
-        launchTestActivity("BySelectorTestDescActivity");
+        launchTestActivity(BySelectorTestDescActivity.class);
 
         // No element has this content description
-        Assert.assertNull(mDevice.findObject(By.desc("No element has this Content Description")));
+        assertNull(mDevice.findObject(By.desc("No element has this Content Description")));
     }
 
     @Test
@@ -283,59 +249,63 @@
         // desc(String)
         try {
             mDevice.findObject(By.desc((String) null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            fail();
+        } catch (NullPointerException expected) {
+        }
 
         // desc(Pattern)
         try {
-            mDevice.findObject(By.desc((Pattern)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.desc((Pattern) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
     }
 
-    // TODO(allenhair): Implement these for desc():
+    // TODO(b/235841286): Implement these for desc():
     // 1. Patterns
     // 2. Runtime Widgets
 
     @Test
     public void testPackage() {
-        launchTestActivity("MainActivity");
+        launchTestActivity(MainActivity.class);
 
         // Full match with string argument
-        Assert.assertNotNull(mDevice.findObject(By.pkg(TEST_APP)));
+        assertNotNull(mDevice.findObject(By.pkg(TEST_APP)));
     }
 
     @Test
     public void testPkgNull() {
         // pkg(String)
         try {
-            mDevice.findObject(By.pkg((String)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.pkg((String) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
 
         // pkg(Pattern)
         try {
-            mDevice.findObject(By.pkg((Pattern)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.pkg((Pattern) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
     }
 
     @Test
     public void testResUniqueId() {
-        launchTestActivity("BySelectorTestResActivity");
+        launchTestActivity(BySelectorTestResActivity.class);
 
         // Unique ID
-        Assert.assertNotNull(mDevice.findObject(By.res(TEST_APP, "unique_id")));
-        Assert.assertNotNull(mDevice.findObject(By.res(TEST_APP + ":id/unique_id")));
+        assertNotNull(mDevice.findObject(By.res(TEST_APP, "unique_id")));
+        assertNotNull(mDevice.findObject(By.res(TEST_APP + ":id/unique_id")));
     }
 
     @Test
     public void testResCommonId() {
-        launchTestActivity("BySelectorTestResActivity");
+        launchTestActivity(BySelectorTestResActivity.class);
 
         // Shared ID
-        Assert.assertNotNull(mDevice.findObject(By.res(TEST_APP, "shared_id")));
-        Assert.assertNotNull(mDevice.findObject(By.res(TEST_APP + ":id/shared_id")));
+        assertNotNull(mDevice.findObject(By.res(TEST_APP, "shared_id")));
+        assertNotNull(mDevice.findObject(By.res(TEST_APP + ":id/shared_id")));
         // 1. Make sure we can see all instances
         // 2. Differentiate between matches by other criteria
     }
@@ -344,126 +314,133 @@
     public void testResNull() {
         // res(String)
         try {
-            mDevice.findObject(By.res((String)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.res((String) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
 
         // res(String, String)
         try {
-            mDevice.findObject(By.res((String)null, "foo"));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.res((String) null, "foo"));
+            fail();
+        } catch (NullPointerException expected) {
+        }
 
         try {
-            mDevice.findObject(By.res("foo", (String)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.res("foo", (String) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
 
         // res(Pattern)
         try {
-            mDevice.findObject(By.res((Pattern)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.res((Pattern) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
     }
 
     @Test
     public void testTextUnique() {
-        launchTestActivity("BySelectorTestTextActivity");
+        launchTestActivity(BySelectorTestTextActivity.class);
 
         // Unique Text
-        Assert.assertNotNull(mDevice.findObject(By.text("Unique Text")));
+        assertNotNull(mDevice.findObject(By.text("Unique Text")));
     }
 
     @Test
     public void testTextCommon() {
-        launchTestActivity("BySelectorTestTextActivity");
+        launchTestActivity(BySelectorTestTextActivity.class);
 
         // Common Text
-        Assert.assertNotNull(mDevice.findObject(By.text("Common Text")));
-        Assert.assertEquals(2, mDevice.findObjects(By.text("Common Text")).size());
+        assertNotNull(mDevice.findObject(By.text("Common Text")));
+        assertEquals(2, mDevice.findObjects(By.text("Common Text")).size());
     }
 
     @Test
     public void testTextNull() {
         // text(String)
         try {
-            mDevice.findObject(By.text((String)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.text((String) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
 
         // text(Pattern)
         try {
-            mDevice.findObject(By.text((Pattern)null));
-            Assert.fail();
-        } catch (NullPointerException e) {}
+            mDevice.findObject(By.text((Pattern) null));
+            fail();
+        } catch (NullPointerException expected) {
+        }
     }
 
     @Test
     public void testHasUniqueChild() {
-        launchTestActivity("BySelectorTestHasChildActivity");
+        launchTestActivity(BySelectorTestHasChildActivity.class);
 
         // Find parent with unique child
         UiObject2 object = mDevice.findObject(By.hasChild(By.res(TEST_APP, "toplevel1_child1")));
-        Assert.assertNotNull(object);
+        assertNotNull(object);
     }
 
     @Test
     public void testHasCommonChild() {
-        launchTestActivity("BySelectorTestHasChildActivity");
+        launchTestActivity(BySelectorTestHasChildActivity.class);
 
         // Find parent(s) with common child
-        Assert.assertNotNull(mDevice.findObject(By.pkg(TEST_APP).hasChild(By.clazz(".TextView"))));
-        Assert.assertEquals(3, mDevice.findObjects(By.pkg(TEST_APP).hasChild(By.clazz(".TextView"))).size());
+        assertNotNull(mDevice.findObject(By.pkg(TEST_APP).hasChild(By.clazz(".TextView"))));
+        assertEquals(3,
+                mDevice.findObjects(By.pkg(TEST_APP).hasChild(By.clazz(".TextView"))).size());
     }
 
     @Test
     public void testGetChildren() {
-        launchTestActivity("BySelectorTestHasChildActivity");
+        launchTestActivity(BySelectorTestHasChildActivity.class);
 
         UiObject2 parent = mDevice.findObject(By.res(TEST_APP, "toplevel2"));
-        Assert.assertEquals(2, parent.getChildren().size());
+        assertEquals(2, parent.getChildren().size());
     }
 
     @Test
     public void testHasMultipleChildren() {
-        launchTestActivity("BySelectorTestHasChildActivity");
+        launchTestActivity(BySelectorTestHasChildActivity.class);
 
         // Select parent with multiple hasChild selectors
         UiObject2 object = mDevice.findObject(By
                 .hasChild(By.res(TEST_APP, "toplevel2_child1"))
                 .hasChild(By.res(TEST_APP, "toplevel2_child2")));
-        Assert.assertNotNull(object);
+        assertNotNull(object);
     }
 
     @Test
     public void testHasMultipleChildrenCollision() {
-        launchTestActivity("BySelectorTestHasChildActivity");
+        launchTestActivity(BySelectorTestHasChildActivity.class);
 
         // Select parent with multiple hasChild selectors, but single child that matches both
         UiObject2 object = mDevice.findObject(By
                 .hasChild(By.res(TEST_APP, "toplevel1_child1"))
                 .hasChild(By.clazz(".TextView")));
-        Assert.assertNotNull(object);
+        assertNotNull(object);
     }
 
     @Test
     public void testHasChildThatHasChild() {
-        launchTestActivity("BySelectorTestHasChildActivity");
+        launchTestActivity(BySelectorTestHasChildActivity.class);
 
         // Select parent with child that has a child
         UiObject2 object = mDevice.findObject(
                 By.hasChild(By.hasChild(By.res(TEST_APP, "toplevel3_container1_child1"))));
-        Assert.assertNotNull(object);
+        assertNotNull(object);
     }
 
     @Test
     public void testHasDescendant() {
-        launchTestActivity("BySelectorTestHasChildActivity");
+        launchTestActivity(BySelectorTestHasChildActivity.class);
 
         // Select a LinearLayout that has a unique descendant
         UiObject2 object = mDevice.findObject(By
                 .clazz(".RelativeLayout")
                 .hasDescendant(By.res(TEST_APP, "toplevel3_container1_child1")));
-        Assert.assertNotNull(object);
+        assertNotNull(object);
     }
 }
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTests.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTests.java
index d7630be..2fac576 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTests.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTests.java
@@ -16,56 +16,40 @@
 
 package androidx.test.uiautomator.testapp;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.UiDevice;
+import static org.junit.Assert.assertTrue;
 
-import org.junit.Assert;
-import org.junit.Before;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.uiautomator.By;
+
 import org.junit.Ignore;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
-@RunWith(AndroidJUnit4.class)
-public class MultiWindowTests {
-
-    private UiDevice mDevice;
-    private static final String TEST_APP = "androidx.test.uiautomator.testapp";
-
-    @Before
-    public void setUp() {
-        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-
-        mDevice.pressHome();
-        mDevice.waitForIdle();
-    }
+public class MultiWindowTests extends BaseTest {
 
     @Test
     @Ignore
-    @SdkSuppress(minSdkVersion=21)
+    @SdkSuppress(minSdkVersion = 21)
     public void testHasBackButton() {
-        Assert.assertTrue(mDevice.hasObject(By.res("com.android.systemui", "back")));
+        assertTrue(mDevice.hasObject(By.res("com.android.systemui", "back")));
     }
 
     @Test
     @Ignore
-    @SdkSuppress(minSdkVersion=21)
+    @SdkSuppress(minSdkVersion = 21)
     public void testHasHomeButton() {
-        Assert.assertTrue(mDevice.hasObject(By.res("com.android.systemui", "home")));
+        assertTrue(mDevice.hasObject(By.res("com.android.systemui", "home")));
     }
 
     @Test
     @Ignore
-    @SdkSuppress(minSdkVersion=21)
+    @SdkSuppress(minSdkVersion = 21)
     public void testHasRecentsButton() {
-        Assert.assertTrue(mDevice.hasObject(By.res("com.android.systemui", "recent_apps")));
+        assertTrue(mDevice.hasObject(By.res("com.android.systemui", "recent_apps")));
     }
 
     @Test
-    @SdkSuppress(minSdkVersion=21)
+    @SdkSuppress(minSdkVersion = 21)
     public void testHasStatusBar() {
-        Assert.assertTrue(mDevice.hasObject(By.res("com.android.systemui", "status_bar")));
+        assertTrue(mDevice.hasObject(By.res("com.android.systemui", "status_bar")));
     }
-}
\ No newline at end of file
+}
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
index 596df2c..b02c52e 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
@@ -16,75 +16,25 @@
 
 package androidx.test.uiautomator.testapp;
 
-import android.content.Context;
-import android.content.Intent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
 import android.graphics.Rect;
 import android.os.SystemClock;
 
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.FlakyTest;
-import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.Direction;
-import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.UiObject2;
-import androidx.test.uiautomator.UiObjectNotFoundException;
 import androidx.test.uiautomator.Until;
 
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
-import java.util.concurrent.TimeoutException;
+public class UiObject2Tests extends BaseTest {
 
-@RunWith(AndroidJUnit4.class)
-public class UiObject2Tests {
-
-    private static final String TEST_APP = "androidx.test.uiautomator.testapp";
-
-    private UiDevice mDevice;
-
-    @Before
-    public void setUp() throws Exception {
-        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-        mDevice.pressHome();
-    }
-
-    private class LaunchActivityRunnable implements Runnable {
-
-        private String mActivity;
-
-        public LaunchActivityRunnable(String activity) {
-            mActivity = activity;
-        }
-
-        @Override
-        public void run() {
-            Context context = ApplicationProvider.getApplicationContext();
-            Intent intent = new Intent()
-                    .setClassName(TEST_APP, String.format("%s.%s", TEST_APP, mActivity))
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-            context.startActivity(intent);
-        }
-    }
-
-    private void launchTestActivity(String activity) {
-        // Launch the test app
-        mDevice.performActionAndWait(new LaunchActivityRunnable(activity), Until.newWindow(), 5000);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        mDevice.pressHome();
-
-        // Wait for the activity to disappear
-        mDevice.wait(Until.gone(By.pkg(TEST_APP)), 5000);
-    }
-
-    /* TODO(allenhair): Implement these tests
+    /* TODO(b/235841473): Implement these tests
     public void testExists() {}
 
     public void testGetChildCount() {}
@@ -93,86 +43,86 @@
     */
 
     @Test
-    public void testGetClassNameButton() throws UiObjectNotFoundException, TimeoutException {
-        launchTestActivity("UiObject2TestGetClassNameActivity");
+    public void testGetClassNameButton() {
+        launchTestActivity(UiObject2TestGetClassNameActivity.class);
 
         UiObject2 object = mDevice.findObject(By.res(TEST_APP, "button"));
-        Assert.assertEquals("android.widget.Button", object.getClassName());
+        assertEquals("android.widget.Button", object.getClassName());
     }
 
     @Test
-    public void testGetClassNameCheckBox() throws UiObjectNotFoundException, TimeoutException {
-        launchTestActivity("UiObject2TestGetClassNameActivity");
+    public void testGetClassNameCheckBox() {
+        launchTestActivity(UiObject2TestGetClassNameActivity.class);
 
         UiObject2 object = mDevice.findObject(By.res(TEST_APP, "check_box"));
-        Assert.assertEquals("android.widget.CheckBox", object.getClassName());
+        assertEquals("android.widget.CheckBox", object.getClassName());
     }
 
     @Test
-    public void testGetClassNameEditText() throws UiObjectNotFoundException, TimeoutException {
-        launchTestActivity("UiObject2TestGetClassNameActivity");
+    public void testGetClassNameEditText() {
+        launchTestActivity(UiObject2TestGetClassNameActivity.class);
 
         UiObject2 object = mDevice.findObject(By.res(TEST_APP, "edit_text"));
-        Assert.assertEquals("android.widget.EditText", object.getClassName());
+        assertEquals("android.widget.EditText", object.getClassName());
     }
 
     @Test
-    public void testGetClassNameProgressBar() throws UiObjectNotFoundException, TimeoutException {
-        launchTestActivity("UiObject2TestGetClassNameActivity");
+    public void testGetClassNameProgressBar() {
+        launchTestActivity(UiObject2TestGetClassNameActivity.class);
 
         UiObject2 object = mDevice.findObject(By.res(TEST_APP, "progress_bar"));
-        Assert.assertEquals("android.widget.ProgressBar", object.getClassName());
+        assertEquals("android.widget.ProgressBar", object.getClassName());
     }
 
     @Test
-    public void testGetClassNameRadioButton() throws UiObjectNotFoundException, TimeoutException {
-        launchTestActivity("UiObject2TestGetClassNameActivity");
+    public void testGetClassNameRadioButton() {
+        launchTestActivity(UiObject2TestGetClassNameActivity.class);
 
         UiObject2 object = mDevice.findObject(By.res(TEST_APP, "radio_button"));
-        Assert.assertEquals("android.widget.RadioButton", object.getClassName());
+        assertEquals("android.widget.RadioButton", object.getClassName());
     }
 
     @Test
-    public void testGetClassNameRatingBar() throws UiObjectNotFoundException, TimeoutException {
-        launchTestActivity("UiObject2TestGetClassNameActivity");
+    public void testGetClassNameRatingBar() {
+        launchTestActivity(UiObject2TestGetClassNameActivity.class);
 
         UiObject2 object = mDevice.findObject(By.res(TEST_APP, "rating_bar"));
-        Assert.assertEquals("android.widget.RatingBar", object.getClassName());
+        assertEquals("android.widget.RatingBar", object.getClassName());
     }
 
     @Test
-    public void testGetClassNameSeekBar() throws UiObjectNotFoundException, TimeoutException {
-        launchTestActivity("UiObject2TestGetClassNameActivity");
+    public void testGetClassNameSeekBar() {
+        launchTestActivity(UiObject2TestGetClassNameActivity.class);
 
         UiObject2 object = mDevice.findObject(By.res(TEST_APP, "seek_bar"));
-        Assert.assertEquals("android.widget.SeekBar", object.getClassName());
+        assertEquals("android.widget.SeekBar", object.getClassName());
     }
 
     @Test
-    public void testGetClassNameSwitch() throws UiObjectNotFoundException, TimeoutException {
-        launchTestActivity("UiObject2TestGetClassNameActivity");
+    public void testGetClassNameSwitch() {
+        launchTestActivity(UiObject2TestGetClassNameActivity.class);
 
         UiObject2 object = mDevice.findObject(By.res(TEST_APP, "switch_toggle"));
-        Assert.assertEquals("android.widget.Switch", object.getClassName());
+        assertEquals("android.widget.Switch", object.getClassName());
     }
 
     @Test
-    public void testGetClassNameTextView() throws UiObjectNotFoundException, TimeoutException {
-        launchTestActivity("UiObject2TestGetClassNameActivity");
+    public void testGetClassNameTextView() {
+        launchTestActivity(UiObject2TestGetClassNameActivity.class);
 
         UiObject2 object = mDevice.findObject(By.res(TEST_APP, "text_view"));
-        Assert.assertEquals("android.widget.TextView", object.getClassName());
+        assertEquals("android.widget.TextView", object.getClassName());
     }
 
     @Test
-    public void testGetClassNameToggleButton() throws UiObjectNotFoundException, TimeoutException {
-        launchTestActivity("UiObject2TestGetClassNameActivity");
+    public void testGetClassNameToggleButton() {
+        launchTestActivity(UiObject2TestGetClassNameActivity.class);
 
         UiObject2 object = mDevice.findObject(By.res(TEST_APP, "toggle_button"));
-        Assert.assertEquals("android.widget.ToggleButton", object.getClassName());
+        assertEquals("android.widget.ToggleButton", object.getClassName());
     }
 
-    /* TODO(allenhair): Implement more tests
+    /* TODO(b/235841473): Implement more tests
     public void testGetContentDescription() {}
 
     public void testGetApplicationPackage() {}
@@ -204,36 +154,36 @@
 
     @Test
     public void testClickButton() {
-        launchTestActivity("UiObject2TestClickActivity");
+        launchTestActivity(UiObject2TestClickActivity.class);
 
         // Find the button and verify its initial state
         UiObject2 button = mDevice.findObject(By.res(TEST_APP, "button"));
-        Assert.assertEquals("Click Me!", button.getText());
+        assertEquals("Click Me!", button.getText());
         SystemClock.sleep(1000);
 
         // Click on the button and verify that the text has changed
         button.click();
         button.wait(Until.textEquals("I've been clicked!"), 10000);
-        Assert.assertEquals("I've been clicked!", button.getText());
+        assertEquals("I've been clicked!", button.getText());
     }
 
     @Test
     public void testClickCheckBox() {
-        launchTestActivity("UiObject2TestClickActivity");
+        launchTestActivity(UiObject2TestClickActivity.class);
 
         // Find the checkbox and verify its initial state
         UiObject2 checkbox = mDevice.findObject(By.res(TEST_APP, "check_box"));
-        Assert.assertEquals(false, checkbox.isChecked());
+        assertFalse(checkbox.isChecked());
 
         // Click on the checkbox and verify that it is now checked
         checkbox.click();
         checkbox.wait(Until.checked(true), 10000);
-        Assert.assertEquals(true, checkbox.isChecked());
+        assertTrue(checkbox.isChecked());
     }
 
     @Test
     public void testClickAndWaitForNewWindow() {
-        launchTestActivity("UiObject2TestClickAndWaitActivity");
+        launchTestActivity(UiObject2TestClickAndWaitActivity.class);
 
         // Click the button and wait for a new window
         UiObject2 button = mDevice.findObject(By.res(TEST_APP, "new_window_button"));
@@ -242,21 +192,21 @@
 
     @Test
     public void testLongClickButton() {
-        launchTestActivity("UiObject2TestLongClickActivity");
+        launchTestActivity(UiObject2TestLongClickActivity.class);
 
         // Find the button and verify its initial state
         UiObject2 button = mDevice.findObject(By.res(TEST_APP, "button"));
-        Assert.assertEquals("Long Click Me!", button.getText());
+        assertEquals("Long Click Me!", button.getText());
 
         // Click on the button and verify that the text has changed
         button.longClick();
         button.wait(Until.textEquals("I've been long clicked!"), 10000);
-        Assert.assertEquals("I've been long clicked!", button.getText());
+        assertEquals("I've been long clicked!", button.getText());
     }
 
     @Test
     public void testPinchIn100Percent() {
-        launchTestActivity("UiObject2TestPinchActivity");
+        launchTestActivity(UiObject2TestPinchActivity.class);
 
         // Find the area to pinch
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
@@ -267,7 +217,7 @@
 
     @Test
     public void testPinchIn75Percent() {
-        launchTestActivity("UiObject2TestPinchActivity");
+        launchTestActivity(UiObject2TestPinchActivity.class);
 
         // Find the area to pinch
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
@@ -278,7 +228,7 @@
 
     @Test
     public void testPinchIn50Percent() {
-        launchTestActivity("UiObject2TestPinchActivity");
+        launchTestActivity(UiObject2TestPinchActivity.class);
 
         // Find the area to pinch
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
@@ -289,7 +239,7 @@
 
     @Test
     public void testPinchIn25Percent() {
-        launchTestActivity("UiObject2TestPinchActivity");
+        launchTestActivity(UiObject2TestPinchActivity.class);
 
         // Find the area to pinch
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
@@ -301,14 +251,14 @@
     @Test
     @FlakyTest
     public void testScrollDown() {
-        launchTestActivity("UiObject2TestVerticalScrollActivity");
+        launchTestActivity(UiObject2TestVerticalScrollActivity.class);
 
         // Make sure we're at the top
-        Assert.assertNotNull(mDevice.findObject(By.res(TEST_APP, "top_text")));
+        assertNotNull(mDevice.findObject(By.res(TEST_APP, "top_text")));
 
         UiObject2 scrollView = mDevice.findObject(By.res(TEST_APP, "scroll_view"));
         Rect bounds = scrollView.getVisibleBounds();
-        float distance = 50000 / (bounds.height() - 2*10);
+        float distance = 50000 / (bounds.height() - 2 * 10);
 
         //
         //scrollView.scroll(Direction.DOWN, 1.0f);
@@ -316,12 +266,12 @@
         //while (scrollView.scroll(Direction.DOWN, 1.0f)) {
         //}
         scrollView.scroll(Direction.DOWN, distance);
-        Assert.assertNotNull(mDevice.findObject(By.res(TEST_APP, "bottom_text")));
+        assertNotNull(mDevice.findObject(By.res(TEST_APP, "bottom_text")));
     }
 
-    /* TODO(allenhair): Fix this test
+    /* TODO(b/235841473): Fix this test
     public void testScrollDistance() {
-        launchTestActivity("UiObject2TestVerticalScrollActivity");
+        launchTestActivity(UiObject2TestVerticalScrollActivity.class);
 
         // Make sure we're at the top
         assertNotNull(mDevice.findObject(By.res(TEST_APP, "top_text")));
@@ -347,21 +297,23 @@
     @Test
     @FlakyTest
     public void testScrollDownToEnd() {
-        launchTestActivity("UiObject2TestVerticalScrollActivity");
+        launchTestActivity(UiObject2TestVerticalScrollActivity.class);
 
         // Make sure we're at the top
-        Assert.assertNotNull(mDevice.findObject(By.res(TEST_APP, "top_text")));
+        assertNotNull(mDevice.findObject(By.res(TEST_APP, "top_text")));
 
         // Scroll as much as we can
         UiObject2 scrollView = mDevice.findObject(By.res(TEST_APP, "scroll_view"));
         scrollView.wait(Until.scrollable(true), 5000);
-        while (scrollView.scroll(Direction.DOWN, 1.0f)) { }
+        while (scrollView.scroll(Direction.DOWN, 1.0f)) {
+            // Continue until bottom.
+        }
 
         // Make sure we're at the bottom
-        Assert.assertNotNull(mDevice.findObject(By.res(TEST_APP, "bottom_text")));
+        assertNotNull(mDevice.findObject(By.res(TEST_APP, "bottom_text")));
     }
 
-    /* TODO(allenhair): Implement these tests
+    /* TODO(b/235841473): Implement these tests
     public void testSetText() {}
 
     public void testWaitForExists() {}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/Processor.java b/work/work-runtime/src/main/java/androidx/work/impl/Processor.java
index 1201feb..d0f1479 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/Processor.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/Processor.java
@@ -120,7 +120,7 @@
                 () -> mWorkDatabase.workSpecDao().getWorkSpec(id.getWorkSpecId())
         );
         if (workSpec == null) {
-            Logger.get().error(TAG, "Didn't find WorkSpec for id " + id);
+            Logger.get().warning(TAG, "Didn't find WorkSpec for id " + id);
             runOnExecuted(id, false);
             return false;
         }