Merge changes from topic "p-shortcut-impl" into main

* changes:
  Widget Picker: Update the launcher integration to support shortcuts
  Widget Picker: Update UI layer to support shortcuts
  Widget Picker: Update data layer to support shortcuts
This commit is contained in:
Shamali Patwa
2025-06-09 11:02:42 -07:00
committed by Android (Google) Code Review
21 changed files with 660 additions and 527 deletions
@@ -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<WidgetPickerComponent.Factory>,
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,
)
)
}
}
}
}
@@ -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<String> = 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<WidgetApp>): List<PickableWidget> {
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<PickableWidget> = mutableListOf()
val usedAppIds: MutableSet<String> = 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
@@ -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<Launcher> {
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
@@ -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)
}
}
@@ -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<List<WidgetApp>>(emptyList())
private val _widgetItemsByPackage = MutableStateFlow<List<WidgetApp>>(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<List<WidgetApp>> = _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<List<PickableWidget>> {
return _widgetItemsByPackage.map { widgets ->
featuredWidgetsDataSource.getFeaturedWidgets(widgets)
}.flowOn(backgroundContext)
return _widgetItemsByPackage
.map { widgets -> featuredWidgetsDataSource.getFeaturedWidgets(widgets) }
.flowOn(backgroundContext)
}
companion object {
private fun Map<PackageItemInfo, List<WidgetItem>>.toPickableWidgets(deviceProfile: DeviceProfile) =
map { (packageItemInfo, widgetItems) ->
val widgetAppId = WidgetAppId(
private fun Map<PackageItemInfo, List<WidgetItem>>.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,
),
)
}
)
}
},
)
}
}
}
@@ -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<Launcher>() {
// 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()
@@ -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<PickableWidget>) =
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
}
}
}
}
@@ -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
}
@@ -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<HostConstraint> = 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<UserHandle>) : HostConstraint()
/** Indicates that the host doesn't support shortcuts. */
data object NoShortcutsConstraint : HostConstraint()
}
@@ -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()
}
@@ -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 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)
},
@@ -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"
-7
View File
@@ -139,13 +139,6 @@
<string-array name="filtered_components" ></string-array>
<string-array name="default_featured_widget_apps" translatable="false">
<item>com.google.android.calendar</item>
<item>com.google.android.deskclock</item>
<item>com.google.android.apps.maps</item>
<item>com.google.android.contacts</item>
<item>com.google.android.apps.chromecast.app</item>
<item>com.google.android.gm</item>
<item>com.google.android.videos</item>
</string-array>
<!-- Swipe back to home related -->
@@ -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<T extends Context & ActivityContext> 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);
}
}
@@ -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(),
@@ -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);