From bb86bde284cd379db505f276b7cd692071748c37 Mon Sep 17 00:00:00 2001 From: shamalip Date: Sun, 8 Jun 2025 17:29:33 +0000 Subject: [PATCH 1/3] Widget Picker: Update data layer to support shortcuts Adds sealed class for widget info that can contain either appwidget info or shortcut info. Also kept the aosp config.xml empty to keep everything eligible for featured Bug: 370950552 Flag: com.android.launcher3.enable_widget_picker_refactor Test: Open picker for homescreen and lockscreen hosts Change-Id: Iceafc2c1234063b16421f90bf00cf96114e9870c --- ...ConfigResourceFeaturedWidgetsDataSource.kt | 57 ++++--- .../repository/WidgetsRepositoryImpl.kt | 142 ++++++++++-------- .../usecase/FilterWidgetsForHostUseCase.kt | 21 ++- .../shared/model/PickableWidget.kt | 52 ++++++- .../shared/model/WidgetHostInfo.kt | 11 +- res/values/config.xml | 7 - .../pm/ShortcutConfigActivityInfo.java | 2 +- 7 files changed, 187 insertions(+), 105 deletions(-) diff --git a/compose/features/com/android/launcher3/widgetpicker/datasource/ConfigResourceFeaturedWidgetsDataSource.kt b/compose/features/com/android/launcher3/widgetpicker/datasource/ConfigResourceFeaturedWidgetsDataSource.kt index d361452286..5c11c7fe84 100644 --- a/compose/features/com/android/launcher3/widgetpicker/datasource/ConfigResourceFeaturedWidgetsDataSource.kt +++ b/compose/features/com/android/launcher3/widgetpicker/datasource/ConfigResourceFeaturedWidgetsDataSource.kt @@ -24,6 +24,7 @@ import com.android.launcher3.dagger.LauncherAppSingleton import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize import com.android.launcher3.widgetpicker.shared.model.PickableWidget import com.android.launcher3.widgetpicker.shared.model.WidgetApp +import com.android.launcher3.widgetpicker.shared.model.isAppWidget import java.util.Arrays import java.util.stream.Collectors import javax.inject.Inject @@ -31,47 +32,61 @@ import javax.inject.Inject /** * An implementation of [FeaturedWidgetsDataSource] that provides featured widgets based on a static * configuration from resources and pre-defined size templates. + * + * Only appwidgets; no shortcuts */ @LauncherAppSingleton -class ConfigResourceFeaturedWidgetsDataSource @Inject constructor( +class ConfigResourceFeaturedWidgetsDataSource +@Inject +constructor( @ApplicationContext private val appContext: Context, - private val idp: InvariantDeviceProfile + private val idp: InvariantDeviceProfile, ) : FeaturedWidgetsDataSource { // the package part in component name e.g. "com.example" in {com.example/widget.Provider} private var eligiblePackages: Set = emptySet() override suspend fun initialize() { if (eligiblePackages.isEmpty()) { - eligiblePackages = Arrays.stream( - appContext.resources.getStringArray(R.array.default_featured_widget_apps) - ).collect(Collectors.toSet()) + eligiblePackages = + Arrays.stream( + appContext.resources.getStringArray(R.array.default_featured_widget_apps) + ) + .collect(Collectors.toSet()) } } override suspend fun getFeaturedWidgets(widgetApps: List): List { - val widgetsByContainerSize = widgetApps - .shuffled() - // pick only one of user profiles - .distinctBy { Pair(it.id.packageName, it.id.category) } - .flatMap { it.widgets } - .filter { eligiblePackages.contains(it.id.componentName.packageName) } - .groupBy { - WidgetPreviewContainerSize( - it.sizeInfo.containerSpanX, - it.sizeInfo.containerSpanY - ) - } + val widgetsByContainerSize = + widgetApps + .shuffled() + // pick only one of user profiles + .distinctBy { Pair(it.id.packageName, it.id.category) } + .flatMap { it.widgets } + .filter { + eligiblePackages.isEmpty() || + eligiblePackages.contains(it.id.componentName.packageName) + } + .groupBy { + WidgetPreviewContainerSize( + it.sizeInfo.containerSpanX, + it.sizeInfo.containerSpanY, + ) + } val selected: MutableList = mutableListOf() val usedAppIds: MutableSet = mutableSetOf() - val sizesToPick = WidgetPreviewContainerSize.pickTemplateForFeaturedWidgets( - idp.getDeviceProfile(appContext) - ) + val sizesToPick = + WidgetPreviewContainerSize.pickTemplateForFeaturedWidgets( + idp.getDeviceProfile(appContext) + ) for (sizeToPick in sizesToPick) { widgetsByContainerSize[sizeToPick]?.shuffled()?.let { items -> for (item in items) { - if (!usedAppIds.contains(item.appId.packageName)) { + if ( + item.widgetInfo.isAppWidget() && + !usedAppIds.contains(item.appId.packageName) + ) { selected.add(item) usedAppIds.add(item.appId.packageName) break diff --git a/compose/features/com/android/launcher3/widgetpicker/repository/WidgetsRepositoryImpl.kt b/compose/features/com/android/launcher3/widgetpicker/repository/WidgetsRepositoryImpl.kt index a49fbc44df..23ab58588a 100644 --- a/compose/features/com/android/launcher3/widgetpicker/repository/WidgetsRepositoryImpl.kt +++ b/compose/features/com/android/launcher3/widgetpicker/repository/WidgetsRepositoryImpl.kt @@ -24,6 +24,7 @@ import com.android.launcher3.dagger.ApplicationContext import com.android.launcher3.model.WidgetItem import com.android.launcher3.model.WidgetsModel import com.android.launcher3.model.data.PackageItemInfo +import com.android.launcher3.pm.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO import com.android.launcher3.util.ComponentKey import com.android.launcher3.util.Executors.MODEL_EXECUTOR import com.android.launcher3.widget.DatabaseWidgetPreviewLoader @@ -36,8 +37,11 @@ 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 javax.inject.Inject +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -51,76 +55,73 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject -import kotlin.coroutines.CoroutineContext /** * An implementation of the [WidgetsRepository] that provides widgets for widget picker using the * [WidgetsModel], [FeaturedWidgetsDataSource] & enables search using the provided * [WidgetsSearchAlgorithm]. */ -class WidgetsRepositoryImpl @Inject constructor( +class WidgetsRepositoryImpl +@Inject +constructor( @ApplicationContext private val appContext: Context, idp: InvariantDeviceProfile, private val widgetsModel: WidgetsModel, private val featuredWidgetsDataSource: FeaturedWidgetsDataSource, private val searchAlgorithm: WidgetsSearchAlgorithm, - @BackgroundContext - private val backgroundContext: CoroutineContext, + @BackgroundContext private val backgroundContext: CoroutineContext, ) : WidgetsRepository { private val deviceProfile = idp.getDeviceProfile(appContext) - private val backgroundScope = CoroutineScope( - SupervisorJob() + backgroundContext + CoroutineName("WidgetsRepository") - ) + private val backgroundScope = + CoroutineScope(SupervisorJob() + backgroundContext + CoroutineName("WidgetsRepository")) - private val _widgetItemsByPackage = - MutableStateFlow>(emptyList()) + private val _widgetItemsByPackage = MutableStateFlow>(emptyList()) private val databaseWidgetPreviewLoader = DatabaseWidgetPreviewLoader(appContext, deviceProfile) override fun initialize() { // TODO(b/419495339): Remove the model executor requirement from widgets model and replace // with scope.launch MODEL_EXECUTOR.execute { - widgetsModel.update(/*packageUser=*/ null) + widgetsModel.update(/* packageUser= */ null) _widgetItemsByPackage.update { widgetsModel.widgetsByPackageItemForPicker.toPickableWidgets(deviceProfile) } } - backgroundScope.launch { - featuredWidgetsDataSource.initialize() - } + backgroundScope.launch { featuredWidgetsDataSource.initialize() } } override fun observeWidgets(): Flow> = _widgetItemsByPackage.asStateFlow() override suspend fun getWidgetPreview(id: WidgetId): WidgetPreview { val componentKey = ComponentKey(id.componentName, id.userHandle) - val widgetItem = widgetsModel.widgetsByComponentKey[componentKey] - ?: return WidgetPreview.PlaceholderWidgetPreview + val widgetItem = + widgetsModel.widgetsByComponentKey[componentKey] + ?: return WidgetPreview.PlaceholderWidgetPreview val previewSizePx = WidgetSizes.getWidgetSizePx(deviceProfile, widgetItem.spanX, widgetItem.spanY) - val preview = withContext(backgroundContext) { - val result = - databaseWidgetPreviewLoader.generatePreviewInfoBg( - widgetItem, - previewSizePx.width, - previewSizePx.height - ) - when { - result.remoteViews != null -> - WidgetPreview.RemoteViewsWidgetPreview(result.remoteViews) + val preview = + withContext(backgroundContext) { + val result = + databaseWidgetPreviewLoader.generatePreviewInfoBg( + widgetItem, + previewSizePx.width, + previewSizePx.height, + ) + when { + result.remoteViews != null -> + WidgetPreview.RemoteViewsWidgetPreview(result.remoteViews) - result.providerInfo != null -> - WidgetPreview.ProviderInfoWidgetPreview(result.providerInfo) + result.providerInfo != null -> + WidgetPreview.ProviderInfoWidgetPreview(result.providerInfo) - result.previewBitmap != null -> - WidgetPreview.BitmapWidgetPreview(result.previewBitmap) + result.previewBitmap != null -> + WidgetPreview.BitmapWidgetPreview(result.previewBitmap) - else -> WidgetPreview.PlaceholderWidgetPreview + else -> WidgetPreview.PlaceholderWidgetPreview + } } - } return preview } @@ -138,29 +139,32 @@ class WidgetsRepositoryImpl @Inject constructor( } override fun getFeaturedWidgets(): Flow> { - return _widgetItemsByPackage.map { widgets -> - featuredWidgetsDataSource.getFeaturedWidgets(widgets) - }.flowOn(backgroundContext) + return _widgetItemsByPackage + .map { widgets -> featuredWidgetsDataSource.getFeaturedWidgets(widgets) } + .flowOn(backgroundContext) } companion object { - private fun Map>.toPickableWidgets(deviceProfile: DeviceProfile) = - map { (packageItemInfo, widgetItems) -> - val widgetAppId = WidgetAppId( + private fun Map>.toPickableWidgets( + deviceProfile: DeviceProfile + ) = map { (packageItemInfo, widgetItems) -> + val widgetAppId = + WidgetAppId( packageName = packageItemInfo.packageName, userHandle = packageItemInfo.user, - category = packageItemInfo.widgetCategory + category = packageItemInfo.widgetCategory, ) - WidgetApp( - id = widgetAppId, - title = packageItemInfo.title, - widgets = widgetItems.filter { it.widgetInfo != null }.map { widgetItem -> + WidgetApp( + id = widgetAppId, + title = packageItemInfo.title, + widgets = + widgetItems.map { widgetItem -> val previewSize = WidgetSizes.getWidgetSizePx( deviceProfile, widgetItem.spanX, - widgetItem.spanY + widgetItem.spanY, ) val containerSpan = WidgetPreviewContainerSize.forItem(widgetItem, deviceProfile) @@ -168,31 +172,43 @@ class WidgetsRepositoryImpl @Inject constructor( WidgetSizes.getWidgetSizePx( deviceProfile, containerSpan.spanX, - containerSpan.spanY + containerSpan.spanY, ) PickableWidget( - id = WidgetId( - componentName = widgetItem.componentName, - userHandle = widgetItem.user - ), + id = + WidgetId( + componentName = widgetItem.componentName, + userHandle = widgetItem.user, + ), appId = widgetAppId, label = widgetItem.label, description = widgetItem.description, - appWidgetProviderInfo = widgetItem.widgetInfo.clone(), - sizeInfo = WidgetSizeInfo( - spanX = widgetItem.widgetInfo.spanX, - spanY = widgetItem.widgetInfo.spanY, - widthPx = previewSize.width, - heightPx = previewSize.height, - containerSpanX = containerSpan.spanX, - containerSpanY = containerSpan.spanY, - containerWidthPx = containerSize.width, - containerHeightPx = containerSize.height - ) + widgetInfo = + if (widgetItem.widgetInfo != null) { + WidgetInfo.AppWidgetInfo( + appWidgetProviderInfo = widgetItem.widgetInfo.clone() + ) + } else { + check(widgetItem.activityInfo is ShortcutConfigActivityInfoVO) + WidgetInfo.ShortcutInfo( + launcherActivityInfo = widgetItem.activityInfo.mInfo + ) + }, + sizeInfo = + WidgetSizeInfo( + spanX = widgetItem.spanX, + spanY = widgetItem.spanY, + widthPx = previewSize.width, + heightPx = previewSize.height, + containerSpanX = containerSpan.spanX, + containerSpanY = containerSpan.spanY, + containerWidthPx = containerSize.width, + containerHeightPx = containerSize.height, + ), ) - } - ) - } + }, + ) + } } } diff --git a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/domain/usecase/FilterWidgetsForHostUseCase.kt b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/domain/usecase/FilterWidgetsForHostUseCase.kt index 07de348c8f..45f7427139 100644 --- a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/domain/usecase/FilterWidgetsForHostUseCase.kt +++ b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/domain/usecase/FilterWidgetsForHostUseCase.kt @@ -21,8 +21,9 @@ import com.android.launcher3.widgetpicker.WidgetPickerSingleton import com.android.launcher3.widgetpicker.shared.model.HostConstraint import com.android.launcher3.widgetpicker.shared.model.PickableWidget import com.android.launcher3.widgetpicker.shared.model.WidgetHostInfo +import com.android.launcher3.widgetpicker.shared.model.isAppWidget +import com.android.launcher3.widgetpicker.shared.model.isShortcut import javax.inject.Inject -import javax.inject.Named /** * A usecase that hosts the business logic of filtering widgets based on host constraints and @@ -34,16 +35,28 @@ class FilterWidgetsForHostUseCase constructor(@WidgetPickerHostInfo private val hostInfo: WidgetHostInfo) { operator fun invoke(widgets: List) = widgets.filter { widget -> + val widgetInfo = widget.widgetInfo + val eligibleForHost = hostInfo.constraints.all { constraint -> when (constraint) { + is HostConstraint.NoShortcutsConstraint -> !widgetInfo.isShortcut() + is HostConstraint.HostUserConstraint -> !constraint.userFilters.contains(widget.id.userHandle) is HostConstraint.HostCategoryConstraint -> { - val widgetCategory = widget.appWidgetProviderInfo.widgetCategory - matchesCategory(constraint.categoryInclusionMask, widgetCategory) && - matchesCategory(constraint.categoryExclusionMask, widgetCategory) + // category applies only to widgets + if (widgetInfo.isAppWidget()) { + val widgetCategory = widgetInfo.appWidgetProviderInfo.widgetCategory + matchesCategory(constraint.categoryInclusionMask, widgetCategory) && + matchesCategory( + constraint.categoryExclusionMask, + widgetCategory, + ) + } else { + true + } } } } diff --git a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/shared/model/PickableWidget.kt b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/shared/model/PickableWidget.kt index 75cee6a722..146692efa4 100644 --- a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/shared/model/PickableWidget.kt +++ b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/shared/model/PickableWidget.kt @@ -17,6 +17,11 @@ package com.android.launcher3.widgetpicker.shared.model import android.appwidget.AppWidgetProviderInfo +import android.content.pm.LauncherActivityInfo +import com.android.launcher3.widgetpicker.shared.model.WidgetInfo.AppWidgetInfo +import com.android.launcher3.widgetpicker.shared.model.WidgetInfo.ShortcutInfo +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract /** * Raw information about a widget that can be considered for display in widget picker list. @@ -28,22 +33,22 @@ import android.appwidget.AppWidgetProviderInfo * @property appId a unique identifier for the app group that this widget could belong to * @property label a user friendly label for the widget. * @property description a user friendly description for the widget - * @property appWidgetProviderInfo widget info associated with the widget as configured by the - * developer; note: this should be a local clone and not the object that was received from - * appwidget manager. + * @property widgetInfo info associated with the widget as configured by the developer shared with + * host when adding a widget; note: this should be a local clone and not the object that was + * received from appwidget manager or package manager. */ data class PickableWidget( val id: WidgetId, val appId: WidgetAppId, val label: String, val description: CharSequence?, - val appWidgetProviderInfo: AppWidgetProviderInfo, + val widgetInfo: WidgetInfo, val sizeInfo: WidgetSizeInfo, ) { // Custom toString to account for the appWidgetProviderInfo. override fun toString(): String = "PickableWidget(id=$id,appId=$appId,label=$label,description=$description," + - "sizeInfo=$sizeInfo,provider=${appWidgetProviderInfo.provider})" + "sizeInfo=$sizeInfo,widgetInfo=${widgetInfo})" } /** @@ -73,3 +78,40 @@ data class WidgetSizeInfo( val containerWidthPx: Int, val containerHeightPx: Int, ) + +/** Information of the widget as configured by the developer. */ +sealed class WidgetInfo { + /** + * @param appWidgetProviderInfo metadata of an installed widgets as received from the appwidget + * manager. + */ + data class AppWidgetInfo(val appWidgetProviderInfo: AppWidgetProviderInfo) : WidgetInfo() + + /** + * @param launcherActivityInfo metadata of an installed deep shortcut as received from the + * package manager. + */ + data class ShortcutInfo(val launcherActivityInfo: LauncherActivityInfo) : WidgetInfo() + + override fun toString(): String { + when (this) { + is AppWidgetInfo -> "WidgetInfo(provider=${this.appWidgetProviderInfo.provider})" + is ShortcutInfo -> "WidgetInfo(activityInfo=${this.launcherActivityInfo.componentName})" + } + return super.toString() + } +} + +/** Returns true if the info is about an app widget. */ +@OptIn(ExperimentalContracts::class) +fun WidgetInfo.isAppWidget(): Boolean { + contract { returns(true) implies (this@isAppWidget is AppWidgetInfo) } + return this is AppWidgetInfo +} + +/** Returns true if the info is about a deep shortcut. */ +@OptIn(ExperimentalContracts::class) +fun WidgetInfo.isShortcut(): Boolean { + contract { returns(true) implies (this@isShortcut is ShortcutInfo) } + return this is ShortcutInfo +} diff --git a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/shared/model/WidgetHostInfo.kt b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/shared/model/WidgetHostInfo.kt index 3622f9f9a2..f6b124916e 100644 --- a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/shared/model/WidgetHostInfo.kt +++ b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/shared/model/WidgetHostInfo.kt @@ -23,17 +23,17 @@ import android.os.UserHandle * * @param title an optional title that should be shown in place of default "Widgets" title. * @param description an optional 1-2 line description to be shown below the title. If not set, no - * description is shown. + * description is shown. * @param constraints constraints around which widgets can be shown in the picker. * @param showDragShadow indicates whether to show drag shadow for the widgets when dragging them; - * can be set to false if host manages drag shadow on its own (e.g. home screen to animate the - * shadow with actual content) + * can be set to false if host manages drag shadow on its own (e.g. home screen to animate the + * shadow with actual content) */ data class WidgetHostInfo( val title: String? = null, val description: String? = null, val constraints: List = emptyList(), - val showDragShadow: Boolean = true + val showDragShadow: Boolean = true, ) /** Various constraints for the widget host. */ @@ -60,4 +60,7 @@ sealed class HostConstraint { * such case, the profile tab shows a generic no widgets available message. */ data class HostUserConstraint(val userFilters: List) : HostConstraint() + + /** Indicates that the host doesn't support shortcuts. */ + data object NoShortcutsConstraint : HostConstraint() } diff --git a/res/values/config.xml b/res/values/config.xml index 05f489c65a..674b093d04 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -139,13 +139,6 @@ - com.google.android.calendar - com.google.android.deskclock - com.google.android.apps.maps - com.google.android.contacts - com.google.android.apps.chromecast.app - com.google.android.gm - com.google.android.videos diff --git a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java index 409174e068..235a46c800 100644 --- a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java +++ b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java @@ -134,7 +134,7 @@ public abstract class ShortcutConfigActivityInfo implements CachedObject { @TargetApi(26) public static class ShortcutConfigActivityInfoVO extends ShortcutConfigActivityInfo { - private final LauncherActivityInfo mInfo; + public final LauncherActivityInfo mInfo; public ShortcutConfigActivityInfoVO(LauncherActivityInfo info) { super(info.getComponentName(), info.getUser(), From d885e21660fb6adfcade91a95b4f82d0fbc8e7fd Mon Sep 17 00:00:00 2001 From: shamalip Date: Sun, 8 Jun 2025 17:30:38 +0000 Subject: [PATCH 2/3] 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 --- .../widgetpicker/ui/components/DragAndDrop.kt | 87 ++++--- .../ui/components/WidgetDetails.kt | 130 +++++------ .../ui/components/WidgetPreview.kt | 218 +++++++++--------- .../widgetpicker/ui/components/WidgetsGrid.kt | 41 ++-- .../ui/components/WidgetsGridTestSamples.kt | 9 +- .../launcher3/widgetpicker/TestUtils.kt | 12 +- .../ui/components/WidgetInteractionsTest.kt | 53 +++-- 7 files changed, 276 insertions(+), 274 deletions(-) diff --git a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/DragAndDrop.kt b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/DragAndDrop.kt index 309eb0f863..8dc79382e7 100644 --- a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/DragAndDrop.kt +++ b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/DragAndDrop.kt @@ -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) } diff --git a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetDetails.kt b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetDetails.kt index 4edcdfd40c..0ad64f804e 100644 --- a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetDetails.kt +++ b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetDetails.kt @@ -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 { diff --git a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetPreview.kt b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetPreview.kt index 7977dd2ddc..2647ef77e8 100644 --- a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetPreview.kt +++ b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetPreview.kt @@ -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 dragState by remember(widgetInfo, showDragShadow) { - derivedStateOf { - DragState( - widgetInfo, - if (showDragShadow) { - ImageBitmapDragShadowBuilder(context, bitmap, scaledBitmapDimensions) - } else { - TransparentDragShadowBuilder - } - ) + 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 + }, + ) + } } - } 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) }, diff --git a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetsGrid.kt b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetsGrid.kt index 5e0ecf8a94..4426ab7254 100644 --- a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetsGrid.kt +++ b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/components/WidgetsGrid.kt @@ -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, 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 } } diff --git a/modules/widgetpicker/tests/multivalentScreenshotTests/src/com/android/launcher3/widgetpicker/ui/components/WidgetsGridTestSamples.kt b/modules/widgetpicker/tests/multivalentScreenshotTests/src/com/android/launcher3/widgetpicker/ui/components/WidgetsGridTestSamples.kt index f86e0ab4f4..29a15cab93 100644 --- a/modules/widgetpicker/tests/multivalentScreenshotTests/src/com/android/launcher3/widgetpicker/ui/components/WidgetsGridTestSamples.kt +++ b/modules/widgetpicker/tests/multivalentScreenshotTests/src/com/android/launcher3/widgetpicker/ui/components/WidgetsGridTestSamples.kt @@ -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, diff --git a/modules/widgetpicker/tests/multivalentTests/src/com/android/launcher3/widgetpicker/TestUtils.kt b/modules/widgetpicker/tests/multivalentTests/src/com/android/launcher3/widgetpicker/TestUtils.kt index d65670177c..890191bea3 100644 --- a/modules/widgetpicker/tests/multivalentTests/src/com/android/launcher3/widgetpicker/TestUtils.kt +++ b/modules/widgetpicker/tests/multivalentTests/src/com/android/launcher3/widgetpicker/TestUtils.kt @@ -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, diff --git a/modules/widgetpicker/tests/multivalentTests/src/com/android/launcher3/widgetpicker/ui/components/WidgetInteractionsTest.kt b/modules/widgetpicker/tests/multivalentTests/src/com/android/launcher3/widgetpicker/ui/components/WidgetInteractionsTest.kt index 6e2fb01f08..83991fab0c 100644 --- a/modules/widgetpicker/tests/multivalentTests/src/com/android/launcher3/widgetpicker/ui/components/WidgetInteractionsTest.kt +++ b/modules/widgetpicker/tests/multivalentTests/src/com/android/launcher3/widgetpicker/ui/components/WidgetInteractionsTest.kt @@ -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() + @get:Rule val composeTestRule = createAndroidComposeRule() @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" From 077234aff5b5e64d1bd99acf8263a42eeb2fa895 Mon Sep 17 00:00:00 2001 From: shamalip Date: Sun, 8 Jun 2025 17:32:28 +0000 Subject: [PATCH 3/3] Widget Picker: Update the launcher integration to support shortcuts Updates drag and drop, tap to add and preview related code. Bug: 370950552 Flag: com.android.launcher3.enable_widget_picker_refactor Test: Drag and drop shortcuts on home screen Change-Id: Iccfe329296dbf4b0770628e77df5cd9b42ac9b21 --- .../WidgetPickerComposeWrapperImpl.kt | 185 +++++++++--------- .../listeners/WidgetPickerAddItemListener.kt | 35 +++- .../listeners/WidgetPickerDragItemListener.kt | 94 +++++---- .../launcher3/widget/AddWidgetConfigTest.kt | 7 +- .../ui/WidgetPickerEventListeners.kt | 16 +- .../android/launcher3/dragndrop/DragView.java | 4 +- .../widget/DatabaseWidgetPreviewLoader.java | 4 +- 7 files changed, 197 insertions(+), 148 deletions(-) diff --git a/compose/features/com/android/launcher3/widgetpicker/WidgetPickerComposeWrapperImpl.kt b/compose/features/com/android/launcher3/widgetpicker/WidgetPickerComposeWrapperImpl.kt index 22a47e96db..c408620089 100644 --- a/compose/features/com/android/launcher3/widgetpicker/WidgetPickerComposeWrapperImpl.kt +++ b/compose/features/com/android/launcher3/widgetpicker/WidgetPickerComposeWrapperImpl.kt @@ -18,8 +18,8 @@ package com.android.launcher3.widgetpicker import android.app.Activity.RESULT_OK import android.content.Context -import androidx.compose.foundation.isSystemInDarkTheme import android.content.Intent +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember @@ -33,7 +33,6 @@ import com.android.launcher3.compose.core.widgetpicker.WidgetPickerComposeWrappe import com.android.launcher3.concurrent.annotations.BackgroundContext import com.android.launcher3.dagger.ApplicationContext import com.android.launcher3.util.ApiWrapper -import com.android.launcher3.widgetpicker.WidgetPickerConfig import com.android.launcher3.widgetpicker.WidgetPickerConfig.Companion.EXTRA_IS_PENDING_WIDGET_DRAG import com.android.launcher3.widgetpicker.data.repository.WidgetAppIconsRepository import com.android.launcher3.widgetpicker.data.repository.WidgetUsersRepository @@ -42,15 +41,16 @@ import com.android.launcher3.widgetpicker.listeners.WidgetPickerAddItemListener import com.android.launcher3.widgetpicker.listeners.WidgetPickerDragItemListener import com.android.launcher3.widgetpicker.shared.model.HostConstraint import com.android.launcher3.widgetpicker.shared.model.WidgetHostInfo +import com.android.launcher3.widgetpicker.shared.model.isAppWidget +import com.android.launcher3.widgetpicker.theme.darkWidgetPickerColors +import com.android.launcher3.widgetpicker.theme.lightWidgetPickerColors import com.android.launcher3.widgetpicker.ui.WidgetInteractionInfo import com.android.launcher3.widgetpicker.ui.WidgetPickerEventListeners import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerTheme -import com.android.launcher3.widgetpicker.theme.darkWidgetPickerColors -import com.android.launcher3.widgetpicker.theme.lightWidgetPickerColors -import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Provider import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.launch /** * An helper that bootstraps widget picker UI (from [WidgetPickerComponent]) in to @@ -58,21 +58,21 @@ import kotlin.coroutines.CoroutineContext * * Sets up the bindings necessary for widget picker component. */ -class WidgetPickerComposeWrapperImpl @Inject constructor( +class WidgetPickerComposeWrapperImpl +@Inject +constructor( private val widgetPickerComponentProvider: Provider, private val widgetsRepository: WidgetsRepository, private val widgetUsersRepository: WidgetUsersRepository, private val widgetAppIconsRepository: WidgetAppIconsRepository, - @BackgroundContext - private val backgroundContext: CoroutineContext, - @ApplicationContext - private val appContext: Context, + @BackgroundContext private val backgroundContext: CoroutineContext, + @ApplicationContext private val appContext: Context, private val apiWrapper: ApiWrapper, ) : WidgetPickerComposeWrapper { override fun showAllWidgets( activity: WidgetPickerActivity, - widgetPickerConfig: WidgetPickerConfig + widgetPickerConfig: WidgetPickerConfig, ) { val widgetPickerComponent = newWidgetPickerComponent(widgetPickerConfig) val callbacks = activity.buildEventListeners(widgetPickerConfig, apiWrapper) @@ -85,11 +85,12 @@ class WidgetPickerComposeWrapperImpl @Inject constructor( val scope = rememberCoroutineScope() val view = LocalView.current - val widgetPickerColors = if (isSystemInDarkTheme()) { - darkWidgetPickerColors() - } else { - lightWidgetPickerColors() - } + val widgetPickerColors = + if (isSystemInDarkTheme()) { + darkWidgetPickerColors() + } else { + lightWidgetPickerColors() + } MaterialTheme { // TODO(b/408283627): Use launcher theme. WidgetPickerTheme(colors = widgetPickerColors) { @@ -99,13 +100,9 @@ class WidgetPickerComposeWrapperImpl @Inject constructor( } DisposableEffect(view) { - scope.launch { - initializeRepositories() - } + scope.launch { initializeRepositories() } - onDispose { - cleanUpRepositories() - } + onDispose { cleanUpRepositories() } } } } @@ -116,19 +113,22 @@ class WidgetPickerComposeWrapperImpl @Inject constructor( private fun newWidgetPickerComponent( widgetPickerConfig: WidgetPickerConfig ): WidgetPickerComponent { - return widgetPickerComponentProvider.get() + return widgetPickerComponentProvider + .get() .build( widgetsRepository = widgetsRepository, widgetUsersRepository = widgetUsersRepository, widgetAppIconsRepository = widgetAppIconsRepository, - widgetHostInfo = WidgetHostInfo( - title = widgetPickerConfig.title - ?: appContext.resources.getString(R.string.widget_button_text), - description = widgetPickerConfig.description, - constraints = widgetPickerConfig.asHostConstraints(), - showDragShadow = !widgetPickerConfig.isForHomeScreen - ), - backgroundContext = backgroundContext + widgetHostInfo = + WidgetHostInfo( + title = + widgetPickerConfig.title + ?: appContext.resources.getString(R.string.widget_button_text), + description = widgetPickerConfig.description, + constraints = widgetPickerConfig.asHostConstraints(), + showDragShadow = !widgetPickerConfig.isForHomeScreen, + ), + backgroundContext = backgroundContext, ) } @@ -150,20 +150,21 @@ class WidgetPickerComposeWrapperImpl @Inject constructor( private fun WidgetPickerActivity.buildEventListeners( widgetPickerConfig: WidgetPickerConfig, - apiWrapper: ApiWrapper - ) = object : WidgetPickerEventListeners { - override fun onClose() { - finish() - } + apiWrapper: ApiWrapper, + ) = + object : WidgetPickerEventListeners { + override fun onClose() { + finish() + } - override fun onWidgetInteraction(widgetInteractionInfo: WidgetInteractionInfo) { - if (widgetPickerConfig.isForHomeScreen) { - handleWidgetInteractionForHomeScreen(widgetInteractionInfo, apiWrapper) - } else { - handleWidgetInteractionForExternalHost(widgetInteractionInfo) + override fun onWidgetInteraction(widgetInteractionInfo: WidgetInteractionInfo) { + if (widgetPickerConfig.isForHomeScreen) { + handleWidgetInteractionForHomeScreen(widgetInteractionInfo, apiWrapper) + } else { + handleWidgetInteractionForExternalHost(widgetInteractionInfo) + } } } - } /** * Handles communication with the home screen about the "add" and "drag" interactions on @@ -171,37 +172,38 @@ class WidgetPickerComposeWrapperImpl @Inject constructor( * * For home screen, we register a listener that is called back when home screen is shown; * - WidgetPickerDragItemListener: bootstraps the drag helper that displays the shadow and - * handles the drag until completion. + * handles the drag until completion. * - WidgetPickerAddItemListener: once launcher is shown, triggers the flow to add the - * widget to workspace. + * widget to workspace. */ private fun WidgetPickerActivity.handleWidgetInteractionForHomeScreen( interactionInfo: WidgetInteractionInfo, - apiWrapper: ApiWrapper + apiWrapper: ApiWrapper, ) { - val interactionListener = when (interactionInfo) { - is WidgetInteractionInfo.WidgetDragInfo -> - WidgetPickerDragItemListener( - mimeType = interactionInfo.mimeType, - appWidgetProviderInfo = interactionInfo.providerInfo, - widgetPreview = interactionInfo.previewInfo, - previewRect = interactionInfo.bounds, - previewWidth = interactionInfo.widthPx - ) + val interactionListener = + when (interactionInfo) { + is WidgetInteractionInfo.WidgetDragInfo -> + WidgetPickerDragItemListener( + mimeType = interactionInfo.mimeType, + widgetInfo = interactionInfo.widgetInfo, + widgetPreview = interactionInfo.previewInfo, + previewRect = interactionInfo.bounds, + previewWidth = interactionInfo.widthPx, + ) - is WidgetInteractionInfo.WidgetAddInfo -> - WidgetPickerAddItemListener(interactionInfo.providerInfo) - } + is WidgetInteractionInfo.WidgetAddInfo -> + WidgetPickerAddItemListener(interactionInfo.widgetInfo) + } Launcher.ACTIVITY_TRACKER.registerCallback( interactionListener, - HOME_SCREEN_WIDGET_INTERACTION_REASON_STRING + HOME_SCREEN_WIDGET_INTERACTION_REASON_STRING, ) startActivity( /*intent=*/ Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_HOME) .setPackage(packageName) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - /*options=*/ apiWrapper.createFadeOutAnimOptions().toBundle() + /*options=*/ apiWrapper.createFadeOutAnimOptions().toBundle(), ) finish() } @@ -209,30 +211,35 @@ class WidgetPickerComposeWrapperImpl @Inject constructor( /** * Handles communication with the external host about the "add" and "drag" interactions on * widgets within widget picker. - * * - In case of drag and drop, finishes the activity with result indicating that there is a - * pending drag [EXTRA_IS_PENDING_WIDGET_DRAG] (that would contain the widget info as part - * of clip data) that the host should be handling. + * pending drag [EXTRA_IS_PENDING_WIDGET_DRAG] (that would contain the widget info as part + * of clip data) that the host should be handling. * - In case of add, finishes the activity with result containing extra information about - * the widget being added (namely [Intent.EXTRA_COMPONENT_NAME] and [Intent.EXTRA_USER]. + * the widget being added (namely [Intent.EXTRA_COMPONENT_NAME] and [Intent.EXTRA_USER]. */ private fun WidgetPickerActivity.handleWidgetInteractionForExternalHost( - widgetInteractionInfo: WidgetInteractionInfo, + widgetInteractionInfo: WidgetInteractionInfo ) { when (widgetInteractionInfo) { is WidgetInteractionInfo.WidgetDragInfo -> - setResult( - RESULT_OK, - Intent().putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true) - ) + setResult(RESULT_OK, Intent().putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true)) is WidgetInteractionInfo.WidgetAddInfo -> { - val providerInfo = widgetInteractionInfo.providerInfo - setResult( - RESULT_OK, Intent() - .putExtra(Intent.EXTRA_COMPONENT_NAME, providerInfo.provider) - .putExtra(Intent.EXTRA_USER, providerInfo.profile) - ) + val widgetInfo = widgetInteractionInfo.widgetInfo + if (widgetInfo.isAppWidget()) { + val providerInfo = widgetInfo.appWidgetProviderInfo + setResult( + RESULT_OK, + Intent().apply { + putExtra(Intent.EXTRA_COMPONENT_NAME, providerInfo.provider) + putExtra(Intent.EXTRA_USER, providerInfo.profile) + }, + ) + } else { + throw IllegalStateException( + "AppWidgetInfo not provided for external host drag" + ) + } } } @@ -240,21 +247,21 @@ class WidgetPickerComposeWrapperImpl @Inject constructor( } /** Builds the host constraints to provide to the widget picker module. */ - fun WidgetPickerConfig.asHostConstraints() = - buildList { - if (filteredUsers.isNotEmpty()) { - add(HostConstraint.HostUserConstraint(filteredUsers)) - } - if (categoryInclusionFilter != 0 - || categoryExclusionFilter != 0 - ) { - add( - HostConstraint.HostCategoryConstraint( - categoryInclusionMask = categoryInclusionFilter, - categoryExclusionMask = categoryExclusionFilter, - ) - ) - } + fun WidgetPickerConfig.asHostConstraints() = buildList { + if (filteredUsers.isNotEmpty()) { + add(HostConstraint.HostUserConstraint(filteredUsers)) } + if (!isForHomeScreen) { + add(HostConstraint.NoShortcutsConstraint) + } + if (categoryInclusionFilter != 0 || categoryExclusionFilter != 0) { + add( + HostConstraint.HostCategoryConstraint( + categoryInclusionMask = categoryInclusionFilter, + categoryExclusionMask = categoryExclusionFilter, + ) + ) + } + } } } diff --git a/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerAddItemListener.kt b/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerAddItemListener.kt index 330b66a14f..988d016bbd 100644 --- a/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerAddItemListener.kt +++ b/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerAddItemListener.kt @@ -16,14 +16,17 @@ package com.android.launcher3.widgetpicker.listeners -import android.appwidget.AppWidgetProviderInfo import android.view.View import com.android.launcher3.Launcher import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY +import com.android.launcher3.PendingAddItemInfo import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.pm.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO import com.android.launcher3.util.ContextTracker.SchedulerCallback import com.android.launcher3.widget.LauncherAppWidgetProviderInfo +import com.android.launcher3.widget.PendingAddShortcutInfo import com.android.launcher3.widget.PendingAddWidgetInfo +import com.android.launcher3.widgetpicker.shared.model.WidgetInfo /** * A callback listener (for tap-to-add flow) that handles adding a widget from a separate widget @@ -31,26 +34,38 @@ import com.android.launcher3.widget.PendingAddWidgetInfo * * Also logs to stats logger once widget is added. */ -class WidgetPickerAddItemListener(private val providerInfo: AppWidgetProviderInfo) : +class WidgetPickerAddItemListener(private val widgetInfo: WidgetInfo) : SchedulerCallback { override fun init(launcher: Launcher?, isHomeStarted: Boolean): Boolean { checkNotNull(launcher) - val launcherProviderInfo = - LauncherAppWidgetProviderInfo.fromProviderInfo(launcher, providerInfo) - val pendingAddWidgetInfo = - PendingAddWidgetInfo(launcherProviderInfo, CONTAINER_WIDGETS_TRAY) + val pendingAddItemInfo: PendingAddItemInfo = + when (widgetInfo) { + is WidgetInfo.AppWidgetInfo -> { + val launcherProviderInfo = + LauncherAppWidgetProviderInfo.fromProviderInfo( + launcher, + widgetInfo.appWidgetProviderInfo, + ) + PendingAddWidgetInfo(launcherProviderInfo, CONTAINER_WIDGETS_TRAY) + } + + is WidgetInfo.ShortcutInfo -> + PendingAddShortcutInfo( + ShortcutConfigActivityInfoVO(widgetInfo.launcherActivityInfo) + ) + } val view = View(launcher) - view.tag = pendingAddWidgetInfo + view.tag = pendingAddItemInfo launcher.accessibilityDelegate?.addToWorkspace( - /*item=*/ pendingAddWidgetInfo, - /*accessibility=*/ false + /*item=*/ pendingAddItemInfo, + /*accessibility=*/ false, ) { launcher.statsLogManager .logger() - .withItemInfo(pendingAddWidgetInfo) + .withItemInfo(pendingAddItemInfo) .log(LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP) } return false // don't receive any more callbacks as we got launcher and handled it diff --git a/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerDragItemListener.kt b/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerDragItemListener.kt index 3b3e8c3a4e..9d13b9bb49 100644 --- a/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerDragItemListener.kt +++ b/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerDragItemListener.kt @@ -16,72 +16,100 @@ package com.android.launcher3.widgetpicker.listeners -import android.appwidget.AppWidgetProviderInfo import android.graphics.Rect import android.view.View import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY +import com.android.launcher3.PendingAddItemInfo import com.android.launcher3.dragndrop.BaseItemDragListener +import com.android.launcher3.pm.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO import com.android.launcher3.widget.DatabaseWidgetPreviewLoader.WidgetPreviewInfo import com.android.launcher3.widget.LauncherAppWidgetProviderInfo +import com.android.launcher3.widget.PendingAddShortcutInfo import com.android.launcher3.widget.PendingAddWidgetInfo import com.android.launcher3.widget.PendingItemDragHelper +import com.android.launcher3.widgetpicker.shared.model.WidgetInfo import com.android.launcher3.widgetpicker.shared.model.WidgetPreview +import com.android.launcher3.widgetpicker.shared.model.isAppWidget /** * A callback listener of type [BaseItemDragListener] that handles widget drag and drop from widget * picker hosted in a separate activity than home screen. * - * Responsible for initializing the [PendingItemDragHelper] that then handles the rest of the - * drag and drop (including showing a drag shadow for the widget). + * Responsible for initializing the [PendingItemDragHelper] that then handles the rest of the drag + * and drop (including showing a drag shadow for the widget). * * @param mimeType a mime type used by widget picker when attaching this listener for a specific - * widget's drag and drop session. - * @param appWidgetProviderInfo provider info of the widget being dragged + * widget's drag and drop session. + * @param widgetInfo metadata of the widget being dragged + * @param widgetPreview provides the preview information for widgets * @param previewRect the bounds of widget's preview offset by the point of long press * @param previewWidth width of the preview as it appears in the widget picker. */ class WidgetPickerDragItemListener( private val mimeType: String, - private val appWidgetProviderInfo: AppWidgetProviderInfo, + private val widgetInfo: WidgetInfo, private val widgetPreview: WidgetPreview, previewRect: Rect, - previewWidth: Int + previewWidth: Int, ) : BaseItemDragListener(previewRect, previewWidth, previewWidth) { override fun getMimeType(): String = mimeType override fun createDragHelper(): PendingItemDragHelper { - val launcherProviderInfo = - LauncherAppWidgetProviderInfo.fromProviderInfo(mLauncher, appWidgetProviderInfo) - val pendingAddWidgetInfo = - PendingAddWidgetInfo(launcherProviderInfo, CONTAINER_WIDGETS_TRAY) + val pendingAddItemInfo: PendingAddItemInfo = + when (widgetInfo) { + is WidgetInfo.AppWidgetInfo -> { + val launcherProviderInfo = + LauncherAppWidgetProviderInfo.fromProviderInfo( + mLauncher, + widgetInfo.appWidgetProviderInfo, + ) + PendingAddWidgetInfo(launcherProviderInfo, CONTAINER_WIDGETS_TRAY) + } + + is WidgetInfo.ShortcutInfo -> + PendingAddShortcutInfo( + ShortcutConfigActivityInfoVO(widgetInfo.launcherActivityInfo) + ) + } val view = View(mLauncher) - view.tag = pendingAddWidgetInfo + view.tag = pendingAddItemInfo val dragHelper = PendingItemDragHelper(view) - val info = WidgetPreviewInfo() - when (widgetPreview) { - is WidgetPreview.BitmapWidgetPreview -> { - info.previewBitmap = widgetPreview.bitmap - info.providerInfo = appWidgetProviderInfo - } - - is WidgetPreview.ProviderInfoWidgetPreview -> { - info.providerInfo = widgetPreview.providerInfo - } - - is WidgetPreview.RemoteViewsWidgetPreview -> { - info.remoteViews = widgetPreview.remoteViews - info.providerInfo = appWidgetProviderInfo - } - - else -> throw IllegalStateException( - "Unsupported preview type when dropping widget to launcher" - ) - } - dragHelper.setWidgetPreviewInfo(info) + if (widgetInfo.isAppWidget()) { + setAppWidgetPreviewInfo(widgetPreview, widgetInfo, dragHelper) + } // shortcut preview is fetched by home screen. return dragHelper } + + private fun setAppWidgetPreviewInfo( + appWidgetPreview: WidgetPreview, + widgetInfo: WidgetInfo.AppWidgetInfo, + dragHelper: PendingItemDragHelper, + ) { + val info = WidgetPreviewInfo() + when (appWidgetPreview) { + is WidgetPreview.BitmapWidgetPreview -> { + info.previewBitmap = appWidgetPreview.bitmap + info.providerInfo = widgetInfo.appWidgetProviderInfo + } + + is WidgetPreview.ProviderInfoWidgetPreview -> { + info.providerInfo = appWidgetPreview.providerInfo + } + + is WidgetPreview.RemoteViewsWidgetPreview -> { + info.remoteViews = appWidgetPreview.remoteViews + info.providerInfo = widgetInfo.appWidgetProviderInfo + } + + else -> + throw IllegalStateException( + "Unsupported preview type when dropping widget to launcher" + ) + } + dragHelper.setWidgetPreviewInfo(info) + } } diff --git a/compose/tests/com/android/launcher3/widget/AddWidgetConfigTest.kt b/compose/tests/com/android/launcher3/widget/AddWidgetConfigTest.kt index 73daacd1af..288604f9eb 100644 --- a/compose/tests/com/android/launcher3/widget/AddWidgetConfigTest.kt +++ b/compose/tests/com/android/launcher3/widget/AddWidgetConfigTest.kt @@ -34,6 +34,7 @@ import com.android.launcher3.util.ui.PortraitLandscapeRunner.PortraitLandscape import com.android.launcher3.util.ui.TestViewHelpers import com.android.launcher3.util.workspace.FavoriteItemsTransaction import com.android.launcher3.widgetpicker.listeners.WidgetPickerAddItemListener +import com.android.launcher3.widgetpicker.shared.model.WidgetInfo import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule @@ -83,10 +84,10 @@ class AddWidgetConfigTest : BaseLauncherActivityTest() { // Add widget to home screen val monitor = WidgetConfigStartupMonitor() - launcherActivity.executeOnLauncher({ l: Launcher -> - val addItemListener = WidgetPickerAddItemListener(widgetInfo) + launcherActivity.executeOnLauncher { l: Launcher -> + val addItemListener = WidgetPickerAddItemListener(WidgetInfo.AppWidgetInfo(widgetInfo)) addItemListener.init(l, /* isHomeStarted= */ true) - }) + } uiDevice.waitForIdle() diff --git a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/WidgetPickerEventListeners.kt b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/WidgetPickerEventListeners.kt index 9973b79398..e515e221bd 100644 --- a/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/WidgetPickerEventListeners.kt +++ b/modules/widgetpicker/src/com/android/launcher3/widgetpicker/ui/WidgetPickerEventListeners.kt @@ -16,13 +16,13 @@ package com.android.launcher3.widgetpicker.ui -import android.appwidget.AppWidgetProviderInfo import android.graphics.Rect +import com.android.launcher3.widgetpicker.shared.model.WidgetInfo import com.android.launcher3.widgetpicker.shared.model.WidgetPreview /** - * General interface that clients can implement to listen to events from different types of - * widget picker. + * General interface that clients can implement to listen to events from different types of widget + * picker. */ interface WidgetPickerEventListeners { /** Called when the widget picker is dismissed. */ @@ -37,7 +37,7 @@ sealed class WidgetInteractionInfo { /** * Information passed in event listener when a widget is dragged. * - * @param providerInfo metadata for the provider of the widget being dragged. + * @param widgetInfo metadata for the provider of the widget being dragged. * @param bounds current bounds of the widget's preview considering the drag offset and scale. * @param widthPx measured width of the preview. * @param heightPx measured height of the preview. @@ -45,7 +45,7 @@ sealed class WidgetInteractionInfo { * @param mimeType a unique mime type set on clip data for the drag session */ data class WidgetDragInfo( - val providerInfo: AppWidgetProviderInfo, + val widgetInfo: WidgetInfo, val bounds: Rect, val widthPx: Int, val heightPx: Int, @@ -56,9 +56,7 @@ sealed class WidgetInteractionInfo { /** * Information passed in event listener when a widget is added using tap to add. * - * @param providerInfo metadata for the provider of the widget being added. + * @param widgetInfo metadata for the provider of the widget being added. */ - data class WidgetAddInfo( - val providerInfo: AppWidgetProviderInfo - ) : WidgetInteractionInfo() + data class WidgetAddInfo(val widgetInfo: WidgetInfo) : WidgetInteractionInfo() } diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java index 89c63511d5..8a76f8532e 100644 --- a/src/com/android/launcher3/dragndrop/DragView.java +++ b/src/com/android/launcher3/dragndrop/DragView.java @@ -21,6 +21,7 @@ import static android.view.View.MeasureSpec.makeMeasureSpec; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; @@ -635,7 +636,8 @@ public abstract class DragView extends Fram // When widgets are dropped from another window, we don't want to remove the // dragView on resume of launcher. if (Flags.enableWidgetPickerRefactor() - && ((DragView) child).mItemType != ITEM_TYPE_APPWIDGET) { + && ((DragView) child).mItemType != ITEM_TYPE_APPWIDGET + && ((DragView) child).mItemType != ITEM_TYPE_DEEP_SHORTCUT) { dragLayer.removeView(child); } } diff --git a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java index 55404d68dc..48f75c73a5 100644 --- a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java +++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java @@ -51,7 +51,6 @@ import com.android.launcher3.pm.ShortcutConfigActivityInfo; import com.android.launcher3.util.CancellableTask; import com.android.launcher3.util.Executors; import com.android.launcher3.util.LooperExecutor; -import com.android.launcher3.views.ActivityContext; import com.android.launcher3.widget.util.WidgetSizes; import java.util.concurrent.ExecutionException; @@ -272,8 +271,7 @@ public class DatabaseWidgetPreviewLoader { private Bitmap generateShortcutPreview( ShortcutConfigActivityInfo info, int maxWidth, int maxHeight) { - int iconSize = ActivityContext.lookupContext( - mContext).getDeviceProfile().getAllAppsProfile().getIconSizePx(); + int iconSize = mDeviceProfile.getAllAppsProfile().getIconSizePx(); int padding = mContext.getResources() .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);