Widget Picker: Update UI layer to support shortcuts

Updates the relevant references to read from the appropriate widget info
 object.

Bug: 370950552
Flag: com.android.launcher3.enable_widget_picker_refactor
Test: Open picker for home screen and lockscreen hosts
Change-Id: I1b2a543b69686d859775618a406901591a32f555
This commit is contained in:
shamalip committed 2025-06-08 20:10:54 +00:00
1 parent bb86bde284
commit d885e21660
7 files changed
+275 -273

No files matched your search

@@ -16,15 +16,16 @@
package com.android.launcher3.widgetpicker.ui.components
import android.appwidget.AppWidgetProviderInfo
import android.content.ClipData
import android.content.ClipDescription
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Point
import android.graphics.Rect
import android.os.UserHandle
import android.view.View
import android.view.View.DragShadowBuilder
import androidx.compose.ui.unit.Dp
@@ -32,33 +33,31 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import androidx.core.graphics.drawable.RoundedBitmapDrawable
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
import java.util.UUID
/**
* Information about the image's dimensions post scaling.
*/
/** Information about the image's dimensions post scaling. */
data class ImageScaledDimensions(
val scale: Float,
val scaledSizeDp: DpSize,
val scaledSizePx: IntSize,
val scaledRadiusDp: Dp,
val scaledRadiusPx: Float
val scaledRadiusPx: Float,
)
/**
* A [DragShadowBuilder] that draws drag shadow using the provided bitmap and image dimensions.
*/
/** A [DragShadowBuilder] that draws drag shadow using the provided bitmap and image dimensions. */
class ImageBitmapDragShadowBuilder(
context: Context,
bitmap: Bitmap,
imageScaledDimensions: ImageScaledDimensions
imageScaledDimensions: ImageScaledDimensions,
) : DragShadowBuilder() {
private val shadowWidth = imageScaledDimensions.scaledSizePx.width
private val shadowHeight = imageScaledDimensions.scaledSizePx.height
private val shadowDrawable: RoundedBitmapDrawable =
RoundedBitmapDrawableFactory.create(context.resources, bitmap)
.apply { cornerRadius = imageScaledDimensions.scaledRadiusPx }
RoundedBitmapDrawableFactory.create(context.resources, bitmap).apply {
cornerRadius = imageScaledDimensions.scaledRadiusPx
}
override fun onProvideShadowMetrics(outShadowSize: Point?, outShadowTouchPoint: Point?) {
outShadowSize?.set(shadowWidth, shadowHeight)
@@ -84,48 +83,62 @@ object TransparentDragShadowBuilder : DragShadowBuilder() {
private const val SHADOW_SIZE = 10
override fun onDrawShadow(canvas: Canvas) {}
override fun onProvideShadowMetrics(outShadowSize: Point, outShadowTouchPoint: Point) {
outShadowSize.set(SHADOW_SIZE, SHADOW_SIZE);
outShadowTouchPoint.set(SHADOW_SIZE / 2, SHADOW_SIZE / 2);
outShadowSize.set(SHADOW_SIZE, SHADOW_SIZE)
outShadowTouchPoint.set(SHADOW_SIZE / 2, SHADOW_SIZE / 2)
}
}
/** State containing information to start a drag for a widget. */
/** State containing information to start a drag for a widget. */
class DragState(
private val widgetInfo: AppWidgetProviderInfo,
private val dragShadowBuilder: DragShadowBuilder
private val widgetInfo: WidgetInfo,
private val dragShadowBuilder: DragShadowBuilder,
) {
private val uniqueId = UUID.randomUUID().toString()
val pickerMimeType = "com.android.launcher3.widgetpicker.drag_and_drop/$uniqueId"
fun startDrag(view: View) {
val clipData = ClipData(
ClipDescription(
// not displayed anywhere; so, set to empty.
/* label= */ "",
arrayOf(
// unique picker specific mime type.
pickerMimeType,
// indicates that the clip item contains an intent (with extras about widget
// info).
ClipDescription.MIMETYPE_TEXT_INTENT
)
),
ClipData.Item(
Intent()
.putExtra(Intent.EXTRA_USER, widgetInfo.profile)
.putExtra(
Intent.EXTRA_COMPONENT_NAME,
widgetInfo.provider
)
val clipData =
ClipData(
ClipDescription(
// not displayed anywhere; so, set to empty.
/* label= */ "",
arrayOf(
// unique picker specific mime type.
pickerMimeType,
// indicates that the clip item contains an intent (with extras about widget
// info).
ClipDescription.MIMETYPE_TEXT_INTENT,
),
),
ClipData.Item(
when (widgetInfo) {
is WidgetInfo.AppWidgetInfo ->
buildIntentForClipData(
user = widgetInfo.appWidgetProviderInfo.profile,
componentName = widgetInfo.appWidgetProviderInfo.provider,
)
is WidgetInfo.ShortcutInfo ->
buildIntentForClipData(
user = widgetInfo.launcherActivityInfo.user,
componentName = widgetInfo.launcherActivityInfo.componentName,
)
}
),
)
)
view.startDragAndDrop(
clipData,
/*shadowBuilder=*/ dragShadowBuilder,
/*myLocalState=*/ null,
View.DRAG_FLAG_GLOBAL
View.DRAG_FLAG_GLOBAL,
)
}
private fun buildIntentForClipData(user: UserHandle, componentName: ComponentName): Intent =
Intent()
.putExtra(Intent.EXTRA_USER, user)
.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName)
}
@@ -36,7 +36,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@@ -90,61 +89,56 @@ fun WidgetDetails(
) {
val haptic = LocalHapticFeedback.current
val interactionSource = remember { MutableInteractionSource() }
val contentDescription = stringResource(
R.string.widget_details_accessibility_label,
widget.label,
widget.sizeInfo.spanX,
widget.sizeInfo.spanY
)
val contentDescription =
stringResource(
R.string.widget_details_accessibility_label,
widget.label,
widget.sizeInfo.spanX,
widget.sizeInfo.spanY,
)
val detailsAlpha: Float by animateFloatAsState(
targetValue = if (showAddButton) INVISIBLE_ALPHA else VISIBLE_ALPHA,
animationSpec = tween(durationMillis = TOGGLE_ANIMATION_DURATION),
label = "detailsAlphaAnimation"
)
val detailsAlpha: Float by
animateFloatAsState(
targetValue = if (showAddButton) INVISIBLE_ALPHA else VISIBLE_ALPHA,
animationSpec = tween(durationMillis = TOGGLE_ANIMATION_DURATION),
label = "detailsAlphaAnimation",
)
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.fillMaxSize()
.clickable(
onClickLabel = if (showAddButton) {
stringResource(R.string.widget_tap_to_hide_add_button_label)
} else {
stringResource(R.string.widget_tap_to_show_add_button_label)
},
interactionSource = interactionSource,
indication = null
) {
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
onAddButtonToggle(
widget.id
)
}
.padding(
horizontal = WidgetDetailsDimensions.horizontalPadding,
vertical = WidgetDetailsDimensions.verticalPadding
)
modifier =
modifier
.fillMaxSize()
.clickable(
onClickLabel =
if (showAddButton) {
stringResource(R.string.widget_tap_to_hide_add_button_label)
} else {
stringResource(R.string.widget_tap_to_show_add_button_label)
},
interactionSource = interactionSource,
indication = null,
) {
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
onAddButtonToggle(widget.id)
}
.padding(
horizontal = WidgetDetailsDimensions.horizontalPadding,
vertical = WidgetDetailsDimensions.verticalPadding,
),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier
.clearAndSetSemantics { this.contentDescription = contentDescription }
.minimumInteractiveComponentSize()
.graphicsLayer { alpha = detailsAlpha }
.fillMaxSize()
modifier =
Modifier.clearAndSetSemantics { this.contentDescription = contentDescription }
.minimumInteractiveComponentSize()
.graphicsLayer { alpha = detailsAlpha }
.fillMaxSize(),
) {
WidgetLabel(
label = widget.label,
appIcon = appIcon,
modifier = Modifier
)
WidgetLabel(label = widget.label, appIcon = appIcon, modifier = Modifier)
if (showAllDetails) {
WidgetSpanSizeLabel(
spanX = widget.sizeInfo.spanX,
spanY = widget.sizeInfo.spanY
)
WidgetSpanSizeLabel(spanX = widget.sizeInfo.spanX, spanY = widget.sizeInfo.spanY)
widget.description?.let { WidgetDescription(it) }
}
}
@@ -152,16 +146,12 @@ fun WidgetDetails(
visible = showAddButton,
modifier = Modifier.fillMaxSize(),
enter = AddButtonDefaults.enterTransition,
exit = AddButtonDefaults.exitTransition
exit = AddButtonDefaults.exitTransition,
) {
AddButton(
widget = widget,
onClick = {
onWidgetAddClick(
WidgetInteractionInfo.WidgetAddInfo(
widget.appWidgetProviderInfo
)
)
onWidgetAddClick(WidgetInteractionInfo.WidgetAddInfo(widget.widgetInfo))
haptic.performHapticFeedback(HapticFeedbackType.Confirm)
},
)
@@ -170,33 +160,28 @@ fun WidgetDetails(
}
@Composable
private fun AddButton(
widget: PickableWidget,
onClick: () -> Unit,
) {
private fun AddButton(widget: PickableWidget, onClick: () -> Unit) {
val accessibleDescription =
stringResource(R.string.widget_tap_to_add_button_content_description, widget.label)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Button(
modifier = Modifier.minimumInteractiveComponentSize(),
contentPadding = AddButtonDimensions.paddingValues,
colors = ButtonDefaults.buttonColors(
containerColor = WidgetPickerTheme.colors.addButtonBackground,
contentColor = WidgetPickerTheme.colors.addButtonContent
),
colors =
ButtonDefaults.buttonColors(
containerColor = WidgetPickerTheme.colors.addButtonBackground,
contentColor = WidgetPickerTheme.colors.addButtonContent,
),
onClick = onClick,
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = null // decorative
contentDescription = null, // decorative
)
Text(
modifier = Modifier.semantics { this.contentDescription = accessibleDescription },
text = stringResource(R.string.widget_tap_to_add_button_label)
text = stringResource(R.string.widget_tap_to_add_button_label),
)
}
}
@@ -208,15 +193,13 @@ private fun WidgetLabel(label: String, appIcon: (@Composable () -> Unit)?, modif
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
) {
if (appIcon != null) {
appIcon()
Spacer(
modifier =
Modifier
.width(WidgetDetailsDimensions.appIconLabelSpacing)
.fillMaxHeight()
Modifier.width(WidgetDetailsDimensions.appIconLabelSpacing).fillMaxHeight()
)
}
Text(
@@ -272,12 +255,7 @@ private object WidgetDetailsDimensions {
}
private object AddButtonDimensions {
val paddingValues = PaddingValues(
start = 8.dp,
top = 11.dp,
end = 16.dp,
bottom = 11.dp
)
val paddingValues = PaddingValues(start = 8.dp, top = 11.dp, end = 16.dp, bottom = 11.dp)
}
private object AddButtonDefaults {
@@ -63,8 +63,10 @@ import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.launcher3.widgetpicker.shared.model.WidgetId
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
import com.android.launcher3.widgetpicker.shared.model.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo
import com.android.launcher3.widgetpicker.shared.model.isAppWidget
import com.android.launcher3.widgetpicker.ui.WidgetInteractionInfo
import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerTheme
import kotlin.math.roundToInt
@@ -75,11 +77,11 @@ fun WidgetPreview(
id: WidgetId,
sizeInfo: WidgetSizeInfo,
preview: WidgetPreview,
appwidgetInfo: AppWidgetProviderInfo,
widgetInfo: WidgetInfo,
modifier: Modifier = Modifier,
showDragShadow: Boolean,
onWidgetInteraction: (WidgetInteractionInfo) -> Unit,
onAddButtonToggle: (WidgetId) -> Unit
onAddButtonToggle: (WidgetId) -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
val haptic = LocalHapticFeedback.current
@@ -93,16 +95,16 @@ fun WidgetPreview(
}
Box(
modifier = modifier
.wrapContentSize()
.clickable(
modifier =
modifier.wrapContentSize().clickable(
interactionSource = interactionSource,
// no ripples for preview taps that toggle the add button.
indication = null
indication = null,
) {
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
onAddButtonToggle(id)
}) {
}
) {
when (preview) {
is WidgetPreview.PlaceholderWidgetPreview ->
PlaceholderWidgetPreview(size = containerSize, widgetRadius = widgetRadius)
@@ -112,30 +114,34 @@ fun WidgetPreview(
bitmap = preview.bitmap,
size = containerSize,
widgetRadius = widgetRadius,
widgetInfo = appwidgetInfo,
widgetInfo = widgetInfo,
showDragShadow = showDragShadow,
onWidgetInteraction = onWidgetInteraction,
)
is WidgetPreview.RemoteViewsWidgetPreview ->
is WidgetPreview.RemoteViewsWidgetPreview -> {
check(widgetInfo.isAppWidget())
RemoteViewsWidgetPreview(
remoteViews = preview.remoteViews,
widgetInfo = appwidgetInfo,
widgetInfo = widgetInfo,
sizeInfo = sizeInfo,
widgetRadius = widgetRadius,
showDragShadow = showDragShadow,
onWidgetInteraction = onWidgetInteraction,
)
}
is WidgetPreview.ProviderInfoWidgetPreview ->
is WidgetPreview.ProviderInfoWidgetPreview -> {
check(widgetInfo.isAppWidget())
RemoteViewsWidgetPreview(
previewLayoutProviderInfo = preview.providerInfo,
widgetInfo = appwidgetInfo,
widgetInfo = widgetInfo,
sizeInfo = sizeInfo,
widgetRadius = widgetRadius,
showDragShadow = showDragShadow,
onWidgetInteraction = onWidgetInteraction,
)
}
}
}
}
@@ -145,8 +151,7 @@ private fun PlaceholderWidgetPreview(size: DpSize, widgetRadius: Dp) {
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier
.width(size.width)
Modifier.width(size.width)
.height(size.height)
.background(
color = WidgetPickerTheme.colors.widgetPlaceholderBackground,
@@ -161,7 +166,7 @@ private fun PlaceholderWidgetPreview(size: DpSize, widgetRadius: Dp) {
private fun BitmapWidgetPreview(
bitmap: Bitmap,
size: DpSize,
widgetInfo: AppWidgetProviderInfo,
widgetInfo: WidgetInfo,
widgetRadius: Dp,
showDragShadow: Boolean,
onWidgetInteraction: (WidgetInteractionInfo) -> Unit,
@@ -170,22 +175,24 @@ private fun BitmapWidgetPreview(
val density = LocalDensity.current
val haptic = LocalHapticFeedback.current
val scaledBitmapDimensions by remember(bitmap, density, size) {
derivedStateOf { bitmap.calculateScaledDimensions(density, size, widgetRadius) }
}
val scaledBitmapDimensions by
remember(bitmap, density, size) {
derivedStateOf { bitmap.calculateScaledDimensions(density, size, widgetRadius) }
}
val dragState by remember(widgetInfo, showDragShadow) {
derivedStateOf {
DragState(
widgetInfo,
if (showDragShadow) {
ImageBitmapDragShadowBuilder(context, bitmap, scaledBitmapDimensions)
} else {
TransparentDragShadowBuilder
}
)
val dragState by
remember(widgetInfo, showDragShadow) {
derivedStateOf {
DragState(
widgetInfo,
if (showDragShadow) {
ImageBitmapDragShadowBuilder(context, bitmap, scaledBitmapDimensions)
} else {
TransparentDragShadowBuilder
},
)
}
}
}
var imagePositionInParent by remember { mutableStateOf(Offset.Zero) }
@@ -199,8 +206,7 @@ private fun BitmapWidgetPreview(
contentDescription = null, // only visual (widget details provides the readable info)
contentScale = ContentScale.FillBounds,
modifier =
Modifier
.onGloballyPositioned { coordinates ->
Modifier.onGloballyPositioned { coordinates ->
imagePositionInParent = coordinates.positionInParent()
}
.pointerInput(bitmap) {
@@ -215,21 +221,19 @@ private fun BitmapWidgetPreview(
calculateImageDragBounds(
scaledBitmapDimensions = scaledBitmapDimensions,
imagePositionInParent = imagePositionInParent,
offset = offset
offset = offset,
)
onWidgetInteraction(
WidgetInteractionInfo.WidgetDragInfo(
mimeType = dragState.pickerMimeType,
providerInfo = widgetInfo,
widgetInfo = widgetInfo,
bounds = bounds,
widthPx = scaledBitmapDimensions.scaledSizePx.width,
heightPx = scaledBitmapDimensions.scaledSizePx.height,
previewInfo = WidgetPreview.BitmapWidgetPreview(
bitmap = bitmap,
),
previewInfo = WidgetPreview.BitmapWidgetPreview(bitmap = bitmap),
)
)
}
},
)
}
.width(scaledBitmapDimensions.scaledSizeDp.width)
@@ -242,26 +246,20 @@ private fun BitmapWidgetPreview(
private fun calculateImageDragBounds(
scaledBitmapDimensions: ImageScaledDimensions,
imagePositionInParent: Offset,
offset: Offset
offset: Offset,
): Rect {
val bounds = Rect()
bounds.left = 0
bounds.top = 0
bounds.right = scaledBitmapDimensions.scaledSizePx.width
bounds.bottom = scaledBitmapDimensions.scaledSizePx.height
val xOffset: Int =
(imagePositionInParent.x - offset.x).roundToInt()
val yOffset: Int =
(imagePositionInParent.y - offset.y).roundToInt()
val xOffset: Int = (imagePositionInParent.x - offset.x).roundToInt()
val yOffset: Int = (imagePositionInParent.y - offset.y).roundToInt()
bounds.offset(xOffset, yOffset)
return bounds
}
private fun Bitmap.calculateScaledDimensions(
density: Density,
size: DpSize,
widgetRadius: Dp
) =
private fun Bitmap.calculateScaledDimensions(density: Density, size: DpSize, widgetRadius: Dp) =
with(density) {
val bitmapSize = DpSize(width = width.toDp(), height = height.toDp())
val bitmapAspectRatio = bitmapSize.width / bitmapSize.height
@@ -269,20 +267,20 @@ private fun Bitmap.calculateScaledDimensions(
// Scale by width if image has larger aspect ratio than the container else by
// height; and avoid cropping the previews.
val scale = if (bitmapAspectRatio > containerAspectRatio) {
size.width / bitmapSize.width
} else {
size.height / bitmapSize.height
}
val scale =
if (bitmapAspectRatio > containerAspectRatio) {
size.width / bitmapSize.width
} else {
size.height / bitmapSize.height
}
val scaledDpSize = DpSize(
width = bitmapSize.width * scale,
height = bitmapSize.height * scale
)
val scaledPxSize = IntSize(
width = scaledDpSize.width.roundToPx(),
height = scaledDpSize.height.roundToPx()
)
val scaledDpSize =
DpSize(width = bitmapSize.width * scale, height = bitmapSize.height * scale)
val scaledPxSize =
IntSize(
width = scaledDpSize.width.roundToPx(),
height = scaledDpSize.height.roundToPx(),
)
val scaledRadius = (widgetRadius * scale).coerceAtMost(widgetRadius).value.roundToInt().dp
ImageScaledDimensions(
@@ -290,7 +288,7 @@ private fun Bitmap.calculateScaledDimensions(
scaledSizePx = scaledPxSize,
scaledSizeDp = scaledDpSize,
scaledRadiusDp = scaledRadius,
scaledRadiusPx = scaledRadius.toPx()
scaledRadiusPx = scaledRadius.toPx(),
)
}
@@ -298,7 +296,7 @@ private fun Bitmap.calculateScaledDimensions(
private fun RemoteViewsWidgetPreview(
remoteViews: RemoteViews? = null,
previewLayoutProviderInfo: AppWidgetProviderInfo? = null,
widgetInfo: AppWidgetProviderInfo,
widgetInfo: WidgetInfo.AppWidgetInfo,
sizeInfo: WidgetSizeInfo,
widgetRadius: Dp,
onWidgetInteraction: (WidgetInteractionInfo) -> Unit,
@@ -308,67 +306,71 @@ private fun RemoteViewsWidgetPreview(
val haptic = LocalHapticFeedback.current
val appWidgetHostView by
remember(sizeInfo, widgetInfo) {
derivedStateOf {
WidgetPreviewHostView(context).apply {
setContainerSizePx(
IntSize(sizeInfo.containerWidthPx, sizeInfo.containerHeightPx)
)
remember(sizeInfo, widgetInfo) {
derivedStateOf {
WidgetPreviewHostView(context).apply {
setContainerSizePx(
IntSize(sizeInfo.containerWidthPx, sizeInfo.containerHeightPx)
)
}
}
}
}
val dragState by remember {
derivedStateOf {
DragState(
widgetInfo = widgetInfo,
dragShadowBuilder = if (showDragShadow) {
DragShadowBuilder(appWidgetHostView)
} else {
TransparentDragShadowBuilder
}
dragShadowBuilder =
if (showDragShadow) {
DragShadowBuilder(appWidgetHostView)
} else {
TransparentDragShadowBuilder
},
)
}
}
key(appWidgetHostView) {
AndroidView(
modifier = Modifier
.pointerInput(appWidgetHostView) {
detectDragGesturesAfterLongPress(
onDrag = { change, _ -> change.consume() },
onDragStart = { offset ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
dragState.startDrag(appWidgetHostView)
modifier =
Modifier.pointerInput(appWidgetHostView) {
detectDragGesturesAfterLongPress(
onDrag = { change, _ -> change.consume() },
onDragStart = { offset ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
dragState.startDrag(appWidgetHostView)
onWidgetInteraction(
WidgetInteractionInfo.WidgetDragInfo(
mimeType = dragState.pickerMimeType,
providerInfo = widgetInfo,
bounds = appWidgetHostView.getDragBoundsForOffset(offset),
widthPx = appWidgetHostView.measuredWidth,
heightPx = appWidgetHostView.measuredHeight,
previewInfo = when {
remoteViews != null ->
WidgetPreview.RemoteViewsWidgetPreview(
remoteViews = remoteViews,
)
onWidgetInteraction(
WidgetInteractionInfo.WidgetDragInfo(
mimeType = dragState.pickerMimeType,
widgetInfo = widgetInfo,
bounds = appWidgetHostView.getDragBoundsForOffset(offset),
widthPx = appWidgetHostView.measuredWidth,
heightPx = appWidgetHostView.measuredHeight,
previewInfo =
when {
remoteViews != null ->
WidgetPreview.RemoteViewsWidgetPreview(
remoteViews = remoteViews
)
previewLayoutProviderInfo != null ->
WidgetPreview.ProviderInfoWidgetPreview(
providerInfo = previewLayoutProviderInfo
)
previewLayoutProviderInfo != null ->
WidgetPreview.ProviderInfoWidgetPreview(
providerInfo = previewLayoutProviderInfo
)
else ->
throw IllegalStateException("No preview during drag")
}
else ->
throw IllegalStateException(
"No preview during drag"
)
},
)
)
)
},
)
}
.wrapContentSize()
.clip(RoundedCornerShape(widgetRadius)),
},
)
}
.wrapContentSize()
.clip(RoundedCornerShape(widgetRadius)),
factory = { appWidgetHostView },
update = { view ->
// if preview.remoteViews is null, initial layout will render.
@@ -376,7 +378,7 @@ private fun RemoteViewsWidgetPreview(
// to be the previewLayout.
view.setAppWidget(
/*appWidgetId=*/ NO_OP_APP_WIDGET_ID,
/*info=*/ previewLayoutProviderInfo ?: widgetInfo,
/*info=*/ previewLayoutProviderInfo ?: widgetInfo.appWidgetProviderInfo,
)
view.updateAppWidget(remoteViews)
},
@@ -61,10 +61,10 @@ import kotlin.math.max
* @param appIcons optional map containing app icons to show in the widget details besides the label
* (when showing the widgets outside of app context e.g. recommendations)
* @param showDragShadow indicates if in a drag and drop session, widget picker should show drag
* shadow containing the preview; if not set, a transparent shadow is rendered and host should
* manage providing a shadow on its own.
* shadow containing the preview; if not set, a transparent shadow is rendered and host should
* manage providing a shadow on its own.
* @param onWidgetInteraction callback invoked when a widget is being dragged and picker has started
* global drag and drop session.
* global drag and drop session.
* @param modifier modifier with parent constraints and additional modifications
*/
@Composable
@@ -93,12 +93,13 @@ fun WidgetsGrid(
addButtonWidgetId = addButtonWidgetId,
onWidgetInteraction = onWidgetInteraction,
onAddButtonToggle = { id ->
addButtonWidgetId = if (id != addButtonWidgetId) {
id
} else {
null
}
}
addButtonWidgetId =
if (id != addButtonWidgetId) {
id
} else {
null
}
},
)
}
}
@@ -155,7 +156,7 @@ private fun WidgetsFlowRow(
appIcons = appIcons,
addButtonWidgetId = addButtonWidgetId,
onWidgetInteraction = onWidgetInteraction,
onAddButtonToggle = onAddButtonToggle
onAddButtonToggle = onAddButtonToggle,
)
},
previewContainerWidthPx = widgetSizeGroup.previewContainerWidthPx,
@@ -184,21 +185,19 @@ private fun Previews(
Box(
contentAlignment = Alignment.BottomCenter,
modifier =
Modifier
.fillMaxSize()
.clearAndSetSemantics {
traversalIndex = index.toFloat()
testTag = buildWidgetPickerTestTag(WIDGET_PREVIEW_TEST_TAG)
},
Modifier.fillMaxSize().clearAndSetSemantics {
traversalIndex = index.toFloat()
testTag = buildWidgetPickerTestTag(WIDGET_PREVIEW_TEST_TAG)
},
) {
WidgetPreview(
id = widgetItem.id,
sizeInfo = widgetItem.sizeInfo,
preview = widgetPreview,
appwidgetInfo = widgetItem.appWidgetProviderInfo,
widgetInfo = widgetItem.widgetInfo,
showDragShadow = showDragShadow,
onWidgetInteraction = onWidgetInteraction,
onAddButtonToggle = onAddButtonToggle
onAddButtonToggle = onAddButtonToggle,
)
}
}
@@ -211,7 +210,7 @@ private fun Details(
addButtonWidgetId: WidgetId?,
appIcons: Map<WidgetAppId, WidgetAppIcon>,
onWidgetInteraction: (WidgetInteractionInfo) -> Unit,
onAddButtonToggle: (WidgetId) -> Unit
onAddButtonToggle: (WidgetId) -> Unit,
) {
widgets.forEachIndexed { index, widgetItem ->
val appId = widgetItem.appId
@@ -376,8 +375,8 @@ private fun Placeable.PlacementScope.placeRows(
// Move to next row
yPosition +=
measuredRow.tallestPreviewHeight +
measuredRow.tallestDetailsHeight +
rowVerticalSpacingPx
measuredRow.tallestDetailsHeight +
rowVerticalSpacingPx
}
}
@@ -30,6 +30,7 @@ import androidx.compose.ui.unit.IntSize
import com.android.launcher3.widgetpicker.shared.model.PickableWidget
import com.android.launcher3.widgetpicker.shared.model.WidgetAppId
import com.android.launcher3.widgetpicker.shared.model.WidgetId
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
import com.android.launcher3.widgetpicker.shared.model.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo
import com.android.launcher3.widgetpicker.tests.R
@@ -253,7 +254,7 @@ object WidgetsGridTestSamples {
containerWidthPx = cellWidth,
containerHeightPx = cellHeight,
),
appWidgetProviderInfo = newAppWidgetInfo("OneByOneProvider"),
widgetInfo = WidgetInfo.AppWidgetInfo(newAppWidgetInfo("OneByOneProvider")),
)
private fun twoByTwo(cellWidth: Int, cellHeight: Int) =
@@ -262,7 +263,7 @@ object WidgetsGridTestSamples {
appId = TEST_WIDGET_APP_ID,
label = "Two by Two",
description = null,
appWidgetProviderInfo = newAppWidgetInfo("TwoByTwoProvider"),
widgetInfo = WidgetInfo.AppWidgetInfo(newAppWidgetInfo("TwoByTwoProvider")),
sizeInfo =
WidgetSizeInfo(
spanX = 2,
@@ -283,7 +284,7 @@ object WidgetsGridTestSamples {
appId = TEST_WIDGET_APP_ID,
label = "Three by two",
description = null,
appWidgetProviderInfo = newAppWidgetInfo("threeByTwoProvider"),
widgetInfo = WidgetInfo.AppWidgetInfo(newAppWidgetInfo("threeByTwoProvider")),
sizeInfo =
WidgetSizeInfo(
spanX = 3,
@@ -304,7 +305,7 @@ object WidgetsGridTestSamples {
appId = TEST_WIDGET_APP_ID,
label = "Four by two",
description = null,
appWidgetProviderInfo = newAppWidgetInfo("FourByTwoProvider"),
widgetInfo = WidgetInfo.AppWidgetInfo(newAppWidgetInfo("FourByTwoProvider")),
sizeInfo =
WidgetSizeInfo(
spanX = 4,
@@ -27,6 +27,7 @@ import com.android.launcher3.widgetpicker.shared.model.PickableWidget
import com.android.launcher3.widgetpicker.shared.model.WidgetApp
import com.android.launcher3.widgetpicker.shared.model.WidgetAppId
import com.android.launcher3.widgetpicker.shared.model.WidgetId
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
import com.android.launcher3.widgetpicker.shared.model.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo
import com.android.launcher3.widgetpicker.shared.model.WidgetUserProfile
@@ -118,10 +119,13 @@ object TestUtils {
appId = finalWidgetAppId,
label = providerClassName,
description = null,
appWidgetProviderInfo = AppWidgetProviderInfo().apply {
widgetCategory = category
provider = ComponentName.createRelative(PACKAGE_NAME, providerClassName)
},
widgetInfo =
WidgetInfo.AppWidgetInfo(
AppWidgetProviderInfo().apply {
widgetCategory = category
provider = ComponentName.createRelative(PACKAGE_NAME, providerClassName)
}
),
sizeInfo =
WidgetSizeInfo(
spanX = 2,
@@ -53,11 +53,9 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@AllowedDevices(allowed = [DeviceProduct.CF_PHONE])
class WidgetInteractionsTest {
@get:Rule
val limitDevicesRule = LimitDevicesRule()
@get:Rule val limitDevicesRule = LimitDevicesRule()
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun tapPreview_andClickAdd() {
@@ -66,7 +64,8 @@ class WidgetInteractionsTest {
composeTestRule.waitForIdle()
// tap on preview for widget 1
composeTestRule.onAllNodesWithTag(PREVIEW_TEST_TAG)
composeTestRule
.onAllNodesWithTag(PREVIEW_TEST_TAG)
.assertCountEquals(2)
.onFirst()
.performClick()
@@ -74,18 +73,19 @@ class WidgetInteractionsTest {
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText(WIDGET_ONE.label).isNotDisplayed() // label text not shown
composeTestRule.onAllNodesWithText(ADD_BUTTON_TEXT).assertCountEquals(1)
composeTestRule.onNodeWithContentDescription(WIDGET_ONE_ADD_BUTTON_CONTENT_DESC)
composeTestRule
.onNodeWithContentDescription(WIDGET_ONE_ADD_BUTTON_CONTENT_DESC)
.assertExists()
.performClick()
composeTestRule.waitForIdle()
// widget interaction callback invoked and correct provider info returned.
composeTestRule.onNodeWithText(WIDGET_ONE.appWidgetProviderInfo.provider.toString())
.assertExists()
composeTestRule.onNodeWithText(WIDGET_ONE.widgetInfo.toString()).assertExists()
// tap again on preview for widget 1
composeTestRule.onAllNodesWithTag(PREVIEW_TEST_TAG)
composeTestRule
.onAllNodesWithTag(PREVIEW_TEST_TAG)
.assertCountEquals(2)
.onFirst()
.performClick()
@@ -103,7 +103,8 @@ class WidgetInteractionsTest {
composeTestRule.waitForIdle()
// tap on preview for widget 1
composeTestRule.onAllNodesWithTag(PREVIEW_TEST_TAG)
composeTestRule
.onAllNodesWithTag(PREVIEW_TEST_TAG)
.assertCountEquals(2)
.onFirst()
.performClick()
@@ -114,11 +115,13 @@ class WidgetInteractionsTest {
composeTestRule.onNodeWithText(WIDGET_ONE.label).isNotDisplayed()
composeTestRule.onNodeWithText(WIDGET_TWO.label).isNotDisplayed()
composeTestRule.onAllNodesWithText(ADD_BUTTON_TEXT).assertCountEquals(1)
composeTestRule.onNodeWithContentDescription(WIDGET_ONE_ADD_BUTTON_CONTENT_DESC)
composeTestRule
.onNodeWithContentDescription(WIDGET_ONE_ADD_BUTTON_CONTENT_DESC)
.assertExists()
// tap on preview for widget 2
composeTestRule.onAllNodesWithTag(PREVIEW_TEST_TAG)
composeTestRule
.onAllNodesWithTag(PREVIEW_TEST_TAG)
.assertCountEquals(2)
.onLast()
.performClick()
@@ -129,11 +132,11 @@ class WidgetInteractionsTest {
composeTestRule.onNodeWithText(WIDGET_ONE.label).isDisplayed()
composeTestRule.onNodeWithText(WIDGET_TWO.label).isNotDisplayed()
composeTestRule.onAllNodesWithText(ADD_BUTTON_TEXT).assertCountEquals(1)
composeTestRule.onNodeWithContentDescription(WIDGET_TWO_ADD_BUTTON_CONTENT_DESC)
composeTestRule
.onNodeWithContentDescription(WIDGET_TWO_ADD_BUTTON_CONTENT_DESC)
.assertExists()
}
@Composable
fun TapToAddTestComposable() {
var provider by remember { mutableStateOf("invalid") }
@@ -149,7 +152,7 @@ class WidgetInteractionsTest {
showDragShadow = false,
onWidgetInteraction = { widgetInteractionInfo ->
if (widgetInteractionInfo is WidgetInteractionInfo.WidgetAddInfo) {
provider = widgetInteractionInfo.providerInfo.provider.toString()
provider = widgetInteractionInfo.widgetInfo.toString()
}
},
)
@@ -160,16 +163,18 @@ class WidgetInteractionsTest {
private val WIDGET_ONE = PERSONAL_TEST_APPS[0].widgets[0]
private val WIDGET_TWO = PERSONAL_TEST_APPS[1].widgets[0]
private val TEST_WIDGET_GROUP = WidgetSizeGroup(
previewContainerHeightPx = 200,
previewContainerWidthPx = 200,
widgets = listOf(WIDGET_ONE, WIDGET_TWO)
)
private val TEST_WIDGET_GROUP =
WidgetSizeGroup(
previewContainerHeightPx = 200,
previewContainerWidthPx = 200,
widgets = listOf(WIDGET_ONE, WIDGET_TWO),
)
private val PREVIEWS = mapOf(
WIDGET_ONE.id to TestUtils.createBitmapPreview(),
WIDGET_TWO.id to TestUtils.createBitmapPreview()
)
private val PREVIEWS =
mapOf(
WIDGET_ONE.id to TestUtils.createBitmapPreview(),
WIDGET_TWO.id to TestUtils.createBitmapPreview(),
)
private val PREVIEW_TEST_TAG = buildWidgetPickerTestTag("widget_preview")
private const val ADD_BUTTON_TEXT = "Add"