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;
}