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.app.Activity.RESULT_OK
import android.content.Context import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
import android.content.Intent import android.content.Intent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember 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.concurrent.annotations.BackgroundContext
import com.android.launcher3.dagger.ApplicationContext import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.util.ApiWrapper 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.WidgetPickerConfig.Companion.EXTRA_IS_PENDING_WIDGET_DRAG
import com.android.launcher3.widgetpicker.data.repository.WidgetAppIconsRepository import com.android.launcher3.widgetpicker.data.repository.WidgetAppIconsRepository
import com.android.launcher3.widgetpicker.data.repository.WidgetUsersRepository 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.listeners.WidgetPickerDragItemListener
import com.android.launcher3.widgetpicker.shared.model.HostConstraint import com.android.launcher3.widgetpicker.shared.model.HostConstraint
import com.android.launcher3.widgetpicker.shared.model.WidgetHostInfo 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.WidgetInteractionInfo
import com.android.launcher3.widgetpicker.ui.WidgetPickerEventListeners import com.android.launcher3.widgetpicker.ui.WidgetPickerEventListeners
import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerTheme 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.Inject
import javax.inject.Provider import javax.inject.Provider
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.launch
/** /**
* An helper that bootstraps widget picker UI (from [WidgetPickerComponent]) in to * 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. * 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 widgetPickerComponentProvider: Provider<WidgetPickerComponent.Factory>,
private val widgetsRepository: WidgetsRepository, private val widgetsRepository: WidgetsRepository,
private val widgetUsersRepository: WidgetUsersRepository, private val widgetUsersRepository: WidgetUsersRepository,
private val widgetAppIconsRepository: WidgetAppIconsRepository, private val widgetAppIconsRepository: WidgetAppIconsRepository,
@BackgroundContext @BackgroundContext private val backgroundContext: CoroutineContext,
private val backgroundContext: CoroutineContext, @ApplicationContext private val appContext: Context,
@ApplicationContext
private val appContext: Context,
private val apiWrapper: ApiWrapper, private val apiWrapper: ApiWrapper,
) : WidgetPickerComposeWrapper { ) : WidgetPickerComposeWrapper {
override fun showAllWidgets( override fun showAllWidgets(
activity: WidgetPickerActivity, activity: WidgetPickerActivity,
widgetPickerConfig: WidgetPickerConfig widgetPickerConfig: WidgetPickerConfig,
) { ) {
val widgetPickerComponent = newWidgetPickerComponent(widgetPickerConfig) val widgetPickerComponent = newWidgetPickerComponent(widgetPickerConfig)
val callbacks = activity.buildEventListeners(widgetPickerConfig, apiWrapper) val callbacks = activity.buildEventListeners(widgetPickerConfig, apiWrapper)
@@ -85,11 +85,12 @@ class WidgetPickerComposeWrapperImpl @Inject constructor(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val view = LocalView.current val view = LocalView.current
val widgetPickerColors = if (isSystemInDarkTheme()) { val widgetPickerColors =
darkWidgetPickerColors() if (isSystemInDarkTheme()) {
} else { darkWidgetPickerColors()
lightWidgetPickerColors() } else {
} lightWidgetPickerColors()
}
MaterialTheme { // TODO(b/408283627): Use launcher theme. MaterialTheme { // TODO(b/408283627): Use launcher theme.
WidgetPickerTheme(colors = widgetPickerColors) { WidgetPickerTheme(colors = widgetPickerColors) {
@@ -99,13 +100,9 @@ class WidgetPickerComposeWrapperImpl @Inject constructor(
} }
DisposableEffect(view) { DisposableEffect(view) {
scope.launch { scope.launch { initializeRepositories() }
initializeRepositories()
}
onDispose { onDispose { cleanUpRepositories() }
cleanUpRepositories()
}
} }
} }
} }
@@ -116,19 +113,22 @@ class WidgetPickerComposeWrapperImpl @Inject constructor(
private fun newWidgetPickerComponent( private fun newWidgetPickerComponent(
widgetPickerConfig: WidgetPickerConfig widgetPickerConfig: WidgetPickerConfig
): WidgetPickerComponent { ): WidgetPickerComponent {
return widgetPickerComponentProvider.get() return widgetPickerComponentProvider
.get()
.build( .build(
widgetsRepository = widgetsRepository, widgetsRepository = widgetsRepository,
widgetUsersRepository = widgetUsersRepository, widgetUsersRepository = widgetUsersRepository,
widgetAppIconsRepository = widgetAppIconsRepository, widgetAppIconsRepository = widgetAppIconsRepository,
widgetHostInfo = WidgetHostInfo( widgetHostInfo =
title = widgetPickerConfig.title WidgetHostInfo(
?: appContext.resources.getString(R.string.widget_button_text), title =
description = widgetPickerConfig.description, widgetPickerConfig.title
constraints = widgetPickerConfig.asHostConstraints(), ?: appContext.resources.getString(R.string.widget_button_text),
showDragShadow = !widgetPickerConfig.isForHomeScreen description = widgetPickerConfig.description,
), constraints = widgetPickerConfig.asHostConstraints(),
backgroundContext = backgroundContext showDragShadow = !widgetPickerConfig.isForHomeScreen,
),
backgroundContext = backgroundContext,
) )
} }
@@ -150,20 +150,21 @@ class WidgetPickerComposeWrapperImpl @Inject constructor(
private fun WidgetPickerActivity.buildEventListeners( private fun WidgetPickerActivity.buildEventListeners(
widgetPickerConfig: WidgetPickerConfig, widgetPickerConfig: WidgetPickerConfig,
apiWrapper: ApiWrapper apiWrapper: ApiWrapper,
) = object : WidgetPickerEventListeners { ) =
override fun onClose() { object : WidgetPickerEventListeners {
finish() override fun onClose() {
} finish()
}
override fun onWidgetInteraction(widgetInteractionInfo: WidgetInteractionInfo) { override fun onWidgetInteraction(widgetInteractionInfo: WidgetInteractionInfo) {
if (widgetPickerConfig.isForHomeScreen) { if (widgetPickerConfig.isForHomeScreen) {
handleWidgetInteractionForHomeScreen(widgetInteractionInfo, apiWrapper) handleWidgetInteractionForHomeScreen(widgetInteractionInfo, apiWrapper)
} else { } else {
handleWidgetInteractionForExternalHost(widgetInteractionInfo) handleWidgetInteractionForExternalHost(widgetInteractionInfo)
}
} }
} }
}
/** /**
* Handles communication with the home screen about the "add" and "drag" interactions on * 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; * 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 * - 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 * - WidgetPickerAddItemListener: once launcher is shown, triggers the flow to add the
* widget to workspace. * widget to workspace.
*/ */
private fun WidgetPickerActivity.handleWidgetInteractionForHomeScreen( private fun WidgetPickerActivity.handleWidgetInteractionForHomeScreen(
interactionInfo: WidgetInteractionInfo, interactionInfo: WidgetInteractionInfo,
apiWrapper: ApiWrapper apiWrapper: ApiWrapper,
) { ) {
val interactionListener = when (interactionInfo) { val interactionListener =
is WidgetInteractionInfo.WidgetDragInfo -> when (interactionInfo) {
WidgetPickerDragItemListener( is WidgetInteractionInfo.WidgetDragInfo ->
mimeType = interactionInfo.mimeType, WidgetPickerDragItemListener(
appWidgetProviderInfo = interactionInfo.providerInfo, mimeType = interactionInfo.mimeType,
widgetPreview = interactionInfo.previewInfo, widgetInfo = interactionInfo.widgetInfo,
previewRect = interactionInfo.bounds, widgetPreview = interactionInfo.previewInfo,
previewWidth = interactionInfo.widthPx previewRect = interactionInfo.bounds,
) previewWidth = interactionInfo.widthPx,
)
is WidgetInteractionInfo.WidgetAddInfo -> is WidgetInteractionInfo.WidgetAddInfo ->
WidgetPickerAddItemListener(interactionInfo.providerInfo) WidgetPickerAddItemListener(interactionInfo.widgetInfo)
} }
Launcher.ACTIVITY_TRACKER.registerCallback( Launcher.ACTIVITY_TRACKER.registerCallback(
interactionListener, interactionListener,
HOME_SCREEN_WIDGET_INTERACTION_REASON_STRING HOME_SCREEN_WIDGET_INTERACTION_REASON_STRING,
) )
startActivity( startActivity(
/*intent=*/ Intent(Intent.ACTION_MAIN) /*intent=*/ Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_HOME) .addCategory(Intent.CATEGORY_HOME)
.setPackage(packageName) .setPackage(packageName)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
/*options=*/ apiWrapper.createFadeOutAnimOptions().toBundle() /*options=*/ apiWrapper.createFadeOutAnimOptions().toBundle(),
) )
finish() finish()
} }
@@ -209,30 +211,35 @@ class WidgetPickerComposeWrapperImpl @Inject constructor(
/** /**
* Handles communication with the external host about the "add" and "drag" interactions on * Handles communication with the external host about the "add" and "drag" interactions on
* widgets within widget picker. * widgets within widget picker.
*
* - In case of drag and drop, finishes the activity with result indicating that there is a * - 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 * pending drag [EXTRA_IS_PENDING_WIDGET_DRAG] (that would contain the widget info as part
* of clip data) that the host should be handling. * of clip data) that the host should be handling.
* - In case of add, finishes the activity with result containing extra information about * - 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( private fun WidgetPickerActivity.handleWidgetInteractionForExternalHost(
widgetInteractionInfo: WidgetInteractionInfo, widgetInteractionInfo: WidgetInteractionInfo
) { ) {
when (widgetInteractionInfo) { when (widgetInteractionInfo) {
is WidgetInteractionInfo.WidgetDragInfo -> is WidgetInteractionInfo.WidgetDragInfo ->
setResult( setResult(RESULT_OK, Intent().putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true))
RESULT_OK,
Intent().putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true)
)
is WidgetInteractionInfo.WidgetAddInfo -> { is WidgetInteractionInfo.WidgetAddInfo -> {
val providerInfo = widgetInteractionInfo.providerInfo val widgetInfo = widgetInteractionInfo.widgetInfo
setResult( if (widgetInfo.isAppWidget()) {
RESULT_OK, Intent() val providerInfo = widgetInfo.appWidgetProviderInfo
.putExtra(Intent.EXTRA_COMPONENT_NAME, providerInfo.provider) setResult(
.putExtra(Intent.EXTRA_USER, providerInfo.profile) 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. */ /** Builds the host constraints to provide to the widget picker module. */
fun WidgetPickerConfig.asHostConstraints() = fun WidgetPickerConfig.asHostConstraints() = buildList {
buildList { if (filteredUsers.isNotEmpty()) {
if (filteredUsers.isNotEmpty()) { add(HostConstraint.HostUserConstraint(filteredUsers))
add(HostConstraint.HostUserConstraint(filteredUsers))
}
if (categoryInclusionFilter != 0
|| categoryExclusionFilter != 0
) {
add(
HostConstraint.HostCategoryConstraint(
categoryInclusionMask = categoryInclusionFilter,
categoryExclusionMask = categoryExclusionFilter,
)
)
}
} }
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.widget.picker.util.WidgetPreviewContainerSize
import com.android.launcher3.widgetpicker.shared.model.PickableWidget import com.android.launcher3.widgetpicker.shared.model.PickableWidget
import com.android.launcher3.widgetpicker.shared.model.WidgetApp import com.android.launcher3.widgetpicker.shared.model.WidgetApp
import com.android.launcher3.widgetpicker.shared.model.isAppWidget
import java.util.Arrays import java.util.Arrays
import java.util.stream.Collectors import java.util.stream.Collectors
import javax.inject.Inject import javax.inject.Inject
@@ -31,47 +32,61 @@ import javax.inject.Inject
/** /**
* An implementation of [FeaturedWidgetsDataSource] that provides featured widgets based on a static * An implementation of [FeaturedWidgetsDataSource] that provides featured widgets based on a static
* configuration from resources and pre-defined size templates. * configuration from resources and pre-defined size templates.
*
* Only appwidgets; no shortcuts
*/ */
@LauncherAppSingleton @LauncherAppSingleton
class ConfigResourceFeaturedWidgetsDataSource @Inject constructor( class ConfigResourceFeaturedWidgetsDataSource
@Inject
constructor(
@ApplicationContext private val appContext: Context, @ApplicationContext private val appContext: Context,
private val idp: InvariantDeviceProfile private val idp: InvariantDeviceProfile,
) : FeaturedWidgetsDataSource { ) : FeaturedWidgetsDataSource {
// the package part in component name e.g. "com.example" in {com.example/widget.Provider} // the package part in component name e.g. "com.example" in {com.example/widget.Provider}
private var eligiblePackages: Set<String> = emptySet() private var eligiblePackages: Set<String> = emptySet()
override suspend fun initialize() { override suspend fun initialize() {
if (eligiblePackages.isEmpty()) { if (eligiblePackages.isEmpty()) {
eligiblePackages = Arrays.stream( eligiblePackages =
appContext.resources.getStringArray(R.array.default_featured_widget_apps) Arrays.stream(
).collect(Collectors.toSet()) appContext.resources.getStringArray(R.array.default_featured_widget_apps)
)
.collect(Collectors.toSet())
} }
} }
override suspend fun getFeaturedWidgets(widgetApps: List<WidgetApp>): List<PickableWidget> { override suspend fun getFeaturedWidgets(widgetApps: List<WidgetApp>): List<PickableWidget> {
val widgetsByContainerSize = widgetApps val widgetsByContainerSize =
.shuffled() widgetApps
// pick only one of user profiles .shuffled()
.distinctBy { Pair(it.id.packageName, it.id.category) } // pick only one of user profiles
.flatMap { it.widgets } .distinctBy { Pair(it.id.packageName, it.id.category) }
.filter { eligiblePackages.contains(it.id.componentName.packageName) } .flatMap { it.widgets }
.groupBy { .filter {
WidgetPreviewContainerSize( eligiblePackages.isEmpty() ||
it.sizeInfo.containerSpanX, eligiblePackages.contains(it.id.componentName.packageName)
it.sizeInfo.containerSpanY }
) .groupBy {
} WidgetPreviewContainerSize(
it.sizeInfo.containerSpanX,
it.sizeInfo.containerSpanY,
)
}
val selected: MutableList<PickableWidget> = mutableListOf() val selected: MutableList<PickableWidget> = mutableListOf()
val usedAppIds: MutableSet<String> = mutableSetOf() val usedAppIds: MutableSet<String> = mutableSetOf()
val sizesToPick = WidgetPreviewContainerSize.pickTemplateForFeaturedWidgets( val sizesToPick =
idp.getDeviceProfile(appContext) WidgetPreviewContainerSize.pickTemplateForFeaturedWidgets(
) idp.getDeviceProfile(appContext)
)
for (sizeToPick in sizesToPick) { for (sizeToPick in sizesToPick) {
widgetsByContainerSize[sizeToPick]?.shuffled()?.let { items -> widgetsByContainerSize[sizeToPick]?.shuffled()?.let { items ->
for (item in items) { for (item in items) {
if (!usedAppIds.contains(item.appId.packageName)) { if (
item.widgetInfo.isAppWidget() &&
!usedAppIds.contains(item.appId.packageName)
) {
selected.add(item) selected.add(item)
usedAppIds.add(item.appId.packageName) usedAppIds.add(item.appId.packageName)
break break
@@ -16,14 +16,17 @@
package com.android.launcher3.widgetpicker.listeners package com.android.launcher3.widgetpicker.listeners
import android.appwidget.AppWidgetProviderInfo
import android.view.View import android.view.View
import com.android.launcher3.Launcher import com.android.launcher3.Launcher
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY 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.logging.StatsLogManager.LauncherEvent
import com.android.launcher3.pm.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO
import com.android.launcher3.util.ContextTracker.SchedulerCallback import com.android.launcher3.util.ContextTracker.SchedulerCallback
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
import com.android.launcher3.widget.PendingAddShortcutInfo
import com.android.launcher3.widget.PendingAddWidgetInfo 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 * 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. * Also logs to stats logger once widget is added.
*/ */
class WidgetPickerAddItemListener(private val providerInfo: AppWidgetProviderInfo) : class WidgetPickerAddItemListener(private val widgetInfo: WidgetInfo) :
SchedulerCallback<Launcher> { SchedulerCallback<Launcher> {
override fun init(launcher: Launcher?, isHomeStarted: Boolean): Boolean { override fun init(launcher: Launcher?, isHomeStarted: Boolean): Boolean {
checkNotNull(launcher) checkNotNull(launcher)
val launcherProviderInfo = val pendingAddItemInfo: PendingAddItemInfo =
LauncherAppWidgetProviderInfo.fromProviderInfo(launcher, providerInfo) when (widgetInfo) {
val pendingAddWidgetInfo = is WidgetInfo.AppWidgetInfo -> {
PendingAddWidgetInfo(launcherProviderInfo, CONTAINER_WIDGETS_TRAY) val launcherProviderInfo =
LauncherAppWidgetProviderInfo.fromProviderInfo(
launcher,
widgetInfo.appWidgetProviderInfo,
)
PendingAddWidgetInfo(launcherProviderInfo, CONTAINER_WIDGETS_TRAY)
}
is WidgetInfo.ShortcutInfo ->
PendingAddShortcutInfo(
ShortcutConfigActivityInfoVO(widgetInfo.launcherActivityInfo)
)
}
val view = View(launcher) val view = View(launcher)
view.tag = pendingAddWidgetInfo view.tag = pendingAddItemInfo
launcher.accessibilityDelegate?.addToWorkspace( launcher.accessibilityDelegate?.addToWorkspace(
/*item=*/ pendingAddWidgetInfo, /*item=*/ pendingAddItemInfo,
/*accessibility=*/ false /*accessibility=*/ false,
) { ) {
launcher.statsLogManager launcher.statsLogManager
.logger() .logger()
.withItemInfo(pendingAddWidgetInfo) .withItemInfo(pendingAddItemInfo)
.log(LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP) .log(LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP)
} }
return false // don't receive any more callbacks as we got launcher and handled it 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 package com.android.launcher3.widgetpicker.listeners
import android.appwidget.AppWidgetProviderInfo
import android.graphics.Rect import android.graphics.Rect
import android.view.View import android.view.View
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY
import com.android.launcher3.PendingAddItemInfo
import com.android.launcher3.dragndrop.BaseItemDragListener 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.DatabaseWidgetPreviewLoader.WidgetPreviewInfo
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
import com.android.launcher3.widget.PendingAddShortcutInfo
import com.android.launcher3.widget.PendingAddWidgetInfo import com.android.launcher3.widget.PendingAddWidgetInfo
import com.android.launcher3.widget.PendingItemDragHelper 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.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.isAppWidget
/** /**
* A callback listener of type [BaseItemDragListener] that handles widget drag and drop from widget * A callback listener of type [BaseItemDragListener] that handles widget drag and drop from widget
* picker hosted in a separate activity than home screen. * picker hosted in a separate activity than home screen.
* *
* Responsible for initializing the [PendingItemDragHelper] that then handles the rest of the * Responsible for initializing the [PendingItemDragHelper] that then handles the rest of the drag
* drag and drop (including showing a drag shadow for the widget). * 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 * @param mimeType a mime type used by widget picker when attaching this listener for a specific
* widget's drag and drop session. * widget's drag and drop session.
* @param appWidgetProviderInfo provider info of the widget being dragged * @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 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. * @param previewWidth width of the preview as it appears in the widget picker.
*/ */
class WidgetPickerDragItemListener( class WidgetPickerDragItemListener(
private val mimeType: String, private val mimeType: String,
private val appWidgetProviderInfo: AppWidgetProviderInfo, private val widgetInfo: WidgetInfo,
private val widgetPreview: WidgetPreview, private val widgetPreview: WidgetPreview,
previewRect: Rect, previewRect: Rect,
previewWidth: Int previewWidth: Int,
) : BaseItemDragListener(previewRect, previewWidth, previewWidth) { ) : BaseItemDragListener(previewRect, previewWidth, previewWidth) {
override fun getMimeType(): String = mimeType override fun getMimeType(): String = mimeType
override fun createDragHelper(): PendingItemDragHelper { override fun createDragHelper(): PendingItemDragHelper {
val launcherProviderInfo = val pendingAddItemInfo: PendingAddItemInfo =
LauncherAppWidgetProviderInfo.fromProviderInfo(mLauncher, appWidgetProviderInfo) when (widgetInfo) {
val pendingAddWidgetInfo = is WidgetInfo.AppWidgetInfo -> {
PendingAddWidgetInfo(launcherProviderInfo, CONTAINER_WIDGETS_TRAY) val launcherProviderInfo =
LauncherAppWidgetProviderInfo.fromProviderInfo(
mLauncher,
widgetInfo.appWidgetProviderInfo,
)
PendingAddWidgetInfo(launcherProviderInfo, CONTAINER_WIDGETS_TRAY)
}
is WidgetInfo.ShortcutInfo ->
PendingAddShortcutInfo(
ShortcutConfigActivityInfoVO(widgetInfo.launcherActivityInfo)
)
}
val view = View(mLauncher) val view = View(mLauncher)
view.tag = pendingAddWidgetInfo view.tag = pendingAddItemInfo
val dragHelper = PendingItemDragHelper(view) val dragHelper = PendingItemDragHelper(view)
val info = WidgetPreviewInfo() if (widgetInfo.isAppWidget()) {
when (widgetPreview) { setAppWidgetPreviewInfo(widgetPreview, widgetInfo, dragHelper)
is WidgetPreview.BitmapWidgetPreview -> { } // shortcut preview is fetched by home screen.
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)
return dragHelper 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.WidgetItem
import com.android.launcher3.model.WidgetsModel import com.android.launcher3.model.WidgetsModel
import com.android.launcher3.model.data.PackageItemInfo 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.ComponentKey
import com.android.launcher3.util.Executors.MODEL_EXECUTOR import com.android.launcher3.util.Executors.MODEL_EXECUTOR
import com.android.launcher3.widget.DatabaseWidgetPreviewLoader 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.WidgetApp
import com.android.launcher3.widgetpicker.shared.model.WidgetAppId import com.android.launcher3.widgetpicker.shared.model.WidgetAppId
import com.android.launcher3.widgetpicker.shared.model.WidgetId 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.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -51,76 +55,73 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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 * An implementation of the [WidgetsRepository] that provides widgets for widget picker using the
* [WidgetsModel], [FeaturedWidgetsDataSource] & enables search using the provided * [WidgetsModel], [FeaturedWidgetsDataSource] & enables search using the provided
* [WidgetsSearchAlgorithm]. * [WidgetsSearchAlgorithm].
*/ */
class WidgetsRepositoryImpl @Inject constructor( class WidgetsRepositoryImpl
@Inject
constructor(
@ApplicationContext private val appContext: Context, @ApplicationContext private val appContext: Context,
idp: InvariantDeviceProfile, idp: InvariantDeviceProfile,
private val widgetsModel: WidgetsModel, private val widgetsModel: WidgetsModel,
private val featuredWidgetsDataSource: FeaturedWidgetsDataSource, private val featuredWidgetsDataSource: FeaturedWidgetsDataSource,
private val searchAlgorithm: WidgetsSearchAlgorithm, private val searchAlgorithm: WidgetsSearchAlgorithm,
@BackgroundContext @BackgroundContext private val backgroundContext: CoroutineContext,
private val backgroundContext: CoroutineContext,
) : WidgetsRepository { ) : WidgetsRepository {
private val deviceProfile = idp.getDeviceProfile(appContext) private val deviceProfile = idp.getDeviceProfile(appContext)
private val backgroundScope = CoroutineScope( private val backgroundScope =
SupervisorJob() + backgroundContext + CoroutineName("WidgetsRepository") CoroutineScope(SupervisorJob() + backgroundContext + CoroutineName("WidgetsRepository"))
)
private val _widgetItemsByPackage = private val _widgetItemsByPackage = MutableStateFlow<List<WidgetApp>>(emptyList())
MutableStateFlow<List<WidgetApp>>(emptyList())
private val databaseWidgetPreviewLoader = DatabaseWidgetPreviewLoader(appContext, deviceProfile) private val databaseWidgetPreviewLoader = DatabaseWidgetPreviewLoader(appContext, deviceProfile)
override fun initialize() { override fun initialize() {
// TODO(b/419495339): Remove the model executor requirement from widgets model and replace // TODO(b/419495339): Remove the model executor requirement from widgets model and replace
// with scope.launch // with scope.launch
MODEL_EXECUTOR.execute { MODEL_EXECUTOR.execute {
widgetsModel.update(/*packageUser=*/ null) widgetsModel.update(/* packageUser= */ null)
_widgetItemsByPackage.update { _widgetItemsByPackage.update {
widgetsModel.widgetsByPackageItemForPicker.toPickableWidgets(deviceProfile) widgetsModel.widgetsByPackageItemForPicker.toPickableWidgets(deviceProfile)
} }
} }
backgroundScope.launch { backgroundScope.launch { featuredWidgetsDataSource.initialize() }
featuredWidgetsDataSource.initialize()
}
} }
override fun observeWidgets(): Flow<List<WidgetApp>> = _widgetItemsByPackage.asStateFlow() override fun observeWidgets(): Flow<List<WidgetApp>> = _widgetItemsByPackage.asStateFlow()
override suspend fun getWidgetPreview(id: WidgetId): WidgetPreview { override suspend fun getWidgetPreview(id: WidgetId): WidgetPreview {
val componentKey = ComponentKey(id.componentName, id.userHandle) val componentKey = ComponentKey(id.componentName, id.userHandle)
val widgetItem = widgetsModel.widgetsByComponentKey[componentKey] val widgetItem =
?: return WidgetPreview.PlaceholderWidgetPreview widgetsModel.widgetsByComponentKey[componentKey]
?: return WidgetPreview.PlaceholderWidgetPreview
val previewSizePx = val previewSizePx =
WidgetSizes.getWidgetSizePx(deviceProfile, widgetItem.spanX, widgetItem.spanY) WidgetSizes.getWidgetSizePx(deviceProfile, widgetItem.spanX, widgetItem.spanY)
val preview = withContext(backgroundContext) { val preview =
val result = withContext(backgroundContext) {
databaseWidgetPreviewLoader.generatePreviewInfoBg( val result =
widgetItem, databaseWidgetPreviewLoader.generatePreviewInfoBg(
previewSizePx.width, widgetItem,
previewSizePx.height previewSizePx.width,
) previewSizePx.height,
when { )
result.remoteViews != null -> when {
WidgetPreview.RemoteViewsWidgetPreview(result.remoteViews) result.remoteViews != null ->
WidgetPreview.RemoteViewsWidgetPreview(result.remoteViews)
result.providerInfo != null -> result.providerInfo != null ->
WidgetPreview.ProviderInfoWidgetPreview(result.providerInfo) WidgetPreview.ProviderInfoWidgetPreview(result.providerInfo)
result.previewBitmap != null -> result.previewBitmap != null ->
WidgetPreview.BitmapWidgetPreview(result.previewBitmap) WidgetPreview.BitmapWidgetPreview(result.previewBitmap)
else -> WidgetPreview.PlaceholderWidgetPreview else -> WidgetPreview.PlaceholderWidgetPreview
}
} }
}
return preview return preview
} }
@@ -138,29 +139,32 @@ class WidgetsRepositoryImpl @Inject constructor(
} }
override fun getFeaturedWidgets(): Flow<List<PickableWidget>> { override fun getFeaturedWidgets(): Flow<List<PickableWidget>> {
return _widgetItemsByPackage.map { widgets -> return _widgetItemsByPackage
featuredWidgetsDataSource.getFeaturedWidgets(widgets) .map { widgets -> featuredWidgetsDataSource.getFeaturedWidgets(widgets) }
}.flowOn(backgroundContext) .flowOn(backgroundContext)
} }
companion object { companion object {
private fun Map<PackageItemInfo, List<WidgetItem>>.toPickableWidgets(deviceProfile: DeviceProfile) = private fun Map<PackageItemInfo, List<WidgetItem>>.toPickableWidgets(
map { (packageItemInfo, widgetItems) -> deviceProfile: DeviceProfile
val widgetAppId = WidgetAppId( ) = map { (packageItemInfo, widgetItems) ->
val widgetAppId =
WidgetAppId(
packageName = packageItemInfo.packageName, packageName = packageItemInfo.packageName,
userHandle = packageItemInfo.user, userHandle = packageItemInfo.user,
category = packageItemInfo.widgetCategory category = packageItemInfo.widgetCategory,
) )
WidgetApp( WidgetApp(
id = widgetAppId, id = widgetAppId,
title = packageItemInfo.title, title = packageItemInfo.title,
widgets = widgetItems.filter { it.widgetInfo != null }.map { widgetItem -> widgets =
widgetItems.map { widgetItem ->
val previewSize = val previewSize =
WidgetSizes.getWidgetSizePx( WidgetSizes.getWidgetSizePx(
deviceProfile, deviceProfile,
widgetItem.spanX, widgetItem.spanX,
widgetItem.spanY widgetItem.spanY,
) )
val containerSpan = val containerSpan =
WidgetPreviewContainerSize.forItem(widgetItem, deviceProfile) WidgetPreviewContainerSize.forItem(widgetItem, deviceProfile)
@@ -168,31 +172,43 @@ class WidgetsRepositoryImpl @Inject constructor(
WidgetSizes.getWidgetSizePx( WidgetSizes.getWidgetSizePx(
deviceProfile, deviceProfile,
containerSpan.spanX, containerSpan.spanX,
containerSpan.spanY containerSpan.spanY,
) )
PickableWidget( PickableWidget(
id = WidgetId( id =
componentName = widgetItem.componentName, WidgetId(
userHandle = widgetItem.user componentName = widgetItem.componentName,
), userHandle = widgetItem.user,
),
appId = widgetAppId, appId = widgetAppId,
label = widgetItem.label, label = widgetItem.label,
description = widgetItem.description, description = widgetItem.description,
appWidgetProviderInfo = widgetItem.widgetInfo.clone(), widgetInfo =
sizeInfo = WidgetSizeInfo( if (widgetItem.widgetInfo != null) {
spanX = widgetItem.widgetInfo.spanX, WidgetInfo.AppWidgetInfo(
spanY = widgetItem.widgetInfo.spanY, appWidgetProviderInfo = widgetItem.widgetInfo.clone()
widthPx = previewSize.width, )
heightPx = previewSize.height, } else {
containerSpanX = containerSpan.spanX, check(widgetItem.activityInfo is ShortcutConfigActivityInfoVO)
containerSpanY = containerSpan.spanY, WidgetInfo.ShortcutInfo(
containerWidthPx = containerSize.width, launcherActivityInfo = widgetItem.activityInfo.mInfo
containerHeightPx = containerSize.height )
) },
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.ui.TestViewHelpers
import com.android.launcher3.util.workspace.FavoriteItemsTransaction import com.android.launcher3.util.workspace.FavoriteItemsTransaction
import com.android.launcher3.widgetpicker.listeners.WidgetPickerAddItemListener import com.android.launcher3.widgetpicker.listeners.WidgetPickerAddItemListener
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@@ -83,10 +84,10 @@ class AddWidgetConfigTest : BaseLauncherActivityTest<Launcher>() {
// Add widget to home screen // Add widget to home screen
val monitor = WidgetConfigStartupMonitor() val monitor = WidgetConfigStartupMonitor()
launcherActivity.executeOnLauncher({ l: Launcher -> launcherActivity.executeOnLauncher { l: Launcher ->
val addItemListener = WidgetPickerAddItemListener(widgetInfo) val addItemListener = WidgetPickerAddItemListener(WidgetInfo.AppWidgetInfo(widgetInfo))
addItemListener.init(l, /* isHomeStarted= */ true) addItemListener.init(l, /* isHomeStarted= */ true)
}) }
uiDevice.waitForIdle() 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.HostConstraint
import com.android.launcher3.widgetpicker.shared.model.PickableWidget import com.android.launcher3.widgetpicker.shared.model.PickableWidget
import com.android.launcher3.widgetpicker.shared.model.WidgetHostInfo 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.Inject
import javax.inject.Named
/** /**
* A usecase that hosts the business logic of filtering widgets based on host constraints and * 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) { constructor(@WidgetPickerHostInfo private val hostInfo: WidgetHostInfo) {
operator fun invoke(widgets: List<PickableWidget>) = operator fun invoke(widgets: List<PickableWidget>) =
widgets.filter { widget -> widgets.filter { widget ->
val widgetInfo = widget.widgetInfo
val eligibleForHost = val eligibleForHost =
hostInfo.constraints.all { constraint -> hostInfo.constraints.all { constraint ->
when (constraint) { when (constraint) {
is HostConstraint.NoShortcutsConstraint -> !widgetInfo.isShortcut()
is HostConstraint.HostUserConstraint -> is HostConstraint.HostUserConstraint ->
!constraint.userFilters.contains(widget.id.userHandle) !constraint.userFilters.contains(widget.id.userHandle)
is HostConstraint.HostCategoryConstraint -> { is HostConstraint.HostCategoryConstraint -> {
val widgetCategory = widget.appWidgetProviderInfo.widgetCategory // category applies only to widgets
matchesCategory(constraint.categoryInclusionMask, widgetCategory) && if (widgetInfo.isAppWidget()) {
matchesCategory(constraint.categoryExclusionMask, widgetCategory) 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 package com.android.launcher3.widgetpicker.shared.model
import android.appwidget.AppWidgetProviderInfo 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. * 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 appId a unique identifier for the app group that this widget could belong to
* @property label a user friendly label for the widget. * @property label a user friendly label for the widget.
* @property description a user friendly description for the widget * @property description a user friendly description for the widget
* @property appWidgetProviderInfo widget info associated with the widget as configured by the * @property widgetInfo info associated with the widget as configured by the developer shared with
* developer; note: this should be a local clone and not the object that was received from * host when adding a widget; note: this should be a local clone and not the object that was
* appwidget manager. * received from appwidget manager or package manager.
*/ */
data class PickableWidget( data class PickableWidget(
val id: WidgetId, val id: WidgetId,
val appId: WidgetAppId, val appId: WidgetAppId,
val label: String, val label: String,
val description: CharSequence?, val description: CharSequence?,
val appWidgetProviderInfo: AppWidgetProviderInfo, val widgetInfo: WidgetInfo,
val sizeInfo: WidgetSizeInfo, val sizeInfo: WidgetSizeInfo,
) { ) {
// Custom toString to account for the appWidgetProviderInfo. // Custom toString to account for the appWidgetProviderInfo.
override fun toString(): String = override fun toString(): String =
"PickableWidget(id=$id,appId=$appId,label=$label,description=$description," + "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 containerWidthPx: Int,
val containerHeightPx: 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 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 * @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 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; * @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 * can be set to false if host manages drag shadow on its own (e.g. home screen to animate the
* shadow with actual content) * shadow with actual content)
*/ */
data class WidgetHostInfo( data class WidgetHostInfo(
val title: String? = null, val title: String? = null,
val description: String? = null, val description: String? = null,
val constraints: List<HostConstraint> = emptyList(), val constraints: List<HostConstraint> = emptyList(),
val showDragShadow: Boolean = true val showDragShadow: Boolean = true,
) )
/** Various constraints for the widget host. */ /** 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. * such case, the profile tab shows a generic no widgets available message.
*/ */
data class HostUserConstraint(val userFilters: List<UserHandle>) : HostConstraint() 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 package com.android.launcher3.widgetpicker.ui
import android.appwidget.AppWidgetProviderInfo
import android.graphics.Rect import android.graphics.Rect
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
import com.android.launcher3.widgetpicker.shared.model.WidgetPreview import com.android.launcher3.widgetpicker.shared.model.WidgetPreview
/** /**
* General interface that clients can implement to listen to events from different types of * General interface that clients can implement to listen to events from different types of widget
* widget picker. * picker.
*/ */
interface WidgetPickerEventListeners { interface WidgetPickerEventListeners {
/** Called when the widget picker is dismissed. */ /** Called when the widget picker is dismissed. */
@@ -37,7 +37,7 @@ sealed class WidgetInteractionInfo {
/** /**
* Information passed in event listener when a widget is dragged. * 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 bounds current bounds of the widget's preview considering the drag offset and scale.
* @param widthPx measured width of the preview. * @param widthPx measured width of the preview.
* @param heightPx measured height 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 * @param mimeType a unique mime type set on clip data for the drag session
*/ */
data class WidgetDragInfo( data class WidgetDragInfo(
val providerInfo: AppWidgetProviderInfo, val widgetInfo: WidgetInfo,
val bounds: Rect, val bounds: Rect,
val widthPx: Int, val widthPx: Int,
val heightPx: 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. * 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( data class WidgetAddInfo(val widgetInfo: WidgetInfo) : WidgetInteractionInfo()
val providerInfo: AppWidgetProviderInfo
) : WidgetInteractionInfo()
} }
@@ -16,15 +16,16 @@
package com.android.launcher3.widgetpicker.ui.components package com.android.launcher3.widgetpicker.ui.components
import android.appwidget.AppWidgetProviderInfo
import android.content.ClipData import android.content.ClipData
import android.content.ClipDescription import android.content.ClipDescription
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Point import android.graphics.Point
import android.graphics.Rect import android.graphics.Rect
import android.os.UserHandle
import android.view.View import android.view.View
import android.view.View.DragShadowBuilder import android.view.View.DragShadowBuilder
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@@ -32,33 +33,31 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.core.graphics.drawable.RoundedBitmapDrawable import androidx.core.graphics.drawable.RoundedBitmapDrawable
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
import java.util.UUID import java.util.UUID
/** /** Information about the image's dimensions post scaling. */
* Information about the image's dimensions post scaling.
*/
data class ImageScaledDimensions( data class ImageScaledDimensions(
val scale: Float, val scale: Float,
val scaledSizeDp: DpSize, val scaledSizeDp: DpSize,
val scaledSizePx: IntSize, val scaledSizePx: IntSize,
val scaledRadiusDp: Dp, 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( class ImageBitmapDragShadowBuilder(
context: Context, context: Context,
bitmap: Bitmap, bitmap: Bitmap,
imageScaledDimensions: ImageScaledDimensions imageScaledDimensions: ImageScaledDimensions,
) : DragShadowBuilder() { ) : DragShadowBuilder() {
private val shadowWidth = imageScaledDimensions.scaledSizePx.width private val shadowWidth = imageScaledDimensions.scaledSizePx.width
private val shadowHeight = imageScaledDimensions.scaledSizePx.height private val shadowHeight = imageScaledDimensions.scaledSizePx.height
private val shadowDrawable: RoundedBitmapDrawable = private val shadowDrawable: RoundedBitmapDrawable =
RoundedBitmapDrawableFactory.create(context.resources, bitmap) RoundedBitmapDrawableFactory.create(context.resources, bitmap).apply {
.apply { cornerRadius = imageScaledDimensions.scaledRadiusPx } cornerRadius = imageScaledDimensions.scaledRadiusPx
}
override fun onProvideShadowMetrics(outShadowSize: Point?, outShadowTouchPoint: Point?) { override fun onProvideShadowMetrics(outShadowSize: Point?, outShadowTouchPoint: Point?) {
outShadowSize?.set(shadowWidth, shadowHeight) outShadowSize?.set(shadowWidth, shadowHeight)
@@ -84,48 +83,62 @@ object TransparentDragShadowBuilder : DragShadowBuilder() {
private const val SHADOW_SIZE = 10 private const val SHADOW_SIZE = 10
override fun onDrawShadow(canvas: Canvas) {} override fun onDrawShadow(canvas: Canvas) {}
override fun onProvideShadowMetrics(outShadowSize: Point, outShadowTouchPoint: Point) { override fun onProvideShadowMetrics(outShadowSize: Point, outShadowTouchPoint: Point) {
outShadowSize.set(SHADOW_SIZE, SHADOW_SIZE); outShadowSize.set(SHADOW_SIZE, SHADOW_SIZE)
outShadowTouchPoint.set(SHADOW_SIZE / 2, SHADOW_SIZE / 2); 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( class DragState(
private val widgetInfo: AppWidgetProviderInfo, private val widgetInfo: WidgetInfo,
private val dragShadowBuilder: DragShadowBuilder private val dragShadowBuilder: DragShadowBuilder,
) { ) {
private val uniqueId = UUID.randomUUID().toString() private val uniqueId = UUID.randomUUID().toString()
val pickerMimeType = "com.android.launcher3.widgetpicker.drag_and_drop/$uniqueId" val pickerMimeType = "com.android.launcher3.widgetpicker.drag_and_drop/$uniqueId"
fun startDrag(view: View) { fun startDrag(view: View) {
val clipData = ClipData( val clipData =
ClipDescription( ClipData(
// not displayed anywhere; so, set to empty. ClipDescription(
/* label= */ "", // not displayed anywhere; so, set to empty.
arrayOf( /* label= */ "",
// unique picker specific mime type. arrayOf(
pickerMimeType, // unique picker specific mime type.
// indicates that the clip item contains an intent (with extras about widget pickerMimeType,
// info). // indicates that the clip item contains an intent (with extras about widget
ClipDescription.MIMETYPE_TEXT_INTENT // info).
) ClipDescription.MIMETYPE_TEXT_INTENT,
), ),
ClipData.Item( ),
Intent() ClipData.Item(
.putExtra(Intent.EXTRA_USER, widgetInfo.profile) when (widgetInfo) {
.putExtra( is WidgetInfo.AppWidgetInfo ->
Intent.EXTRA_COMPONENT_NAME, buildIntentForClipData(
widgetInfo.provider user = widgetInfo.appWidgetProviderInfo.profile,
) componentName = widgetInfo.appWidgetProviderInfo.provider,
)
is WidgetInfo.ShortcutInfo ->
buildIntentForClipData(
user = widgetInfo.launcherActivityInfo.user,
componentName = widgetInfo.launcherActivityInfo.componentName,
)
}
),
) )
)
view.startDragAndDrop( view.startDragAndDrop(
clipData, clipData,
/*shadowBuilder=*/ dragShadowBuilder, /*shadowBuilder=*/ dragShadowBuilder,
/*myLocalState=*/ null, /*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.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -90,61 +89,56 @@ fun WidgetDetails(
) { ) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val contentDescription = stringResource( val contentDescription =
R.string.widget_details_accessibility_label, stringResource(
widget.label, R.string.widget_details_accessibility_label,
widget.sizeInfo.spanX, widget.label,
widget.sizeInfo.spanY widget.sizeInfo.spanX,
) widget.sizeInfo.spanY,
)
val detailsAlpha: Float by animateFloatAsState( val detailsAlpha: Float by
targetValue = if (showAddButton) INVISIBLE_ALPHA else VISIBLE_ALPHA, animateFloatAsState(
animationSpec = tween(durationMillis = TOGGLE_ANIMATION_DURATION), targetValue = if (showAddButton) INVISIBLE_ALPHA else VISIBLE_ALPHA,
label = "detailsAlphaAnimation" animationSpec = tween(durationMillis = TOGGLE_ANIMATION_DURATION),
) label = "detailsAlphaAnimation",
)
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = modifier modifier =
.fillMaxSize() modifier
.clickable( .fillMaxSize()
onClickLabel = if (showAddButton) { .clickable(
stringResource(R.string.widget_tap_to_hide_add_button_label) onClickLabel =
} else { if (showAddButton) {
stringResource(R.string.widget_tap_to_show_add_button_label) stringResource(R.string.widget_tap_to_hide_add_button_label)
}, } else {
interactionSource = interactionSource, stringResource(R.string.widget_tap_to_show_add_button_label)
indication = null },
) { interactionSource = interactionSource,
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey) indication = null,
onAddButtonToggle( ) {
widget.id haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
) onAddButtonToggle(widget.id)
} }
.padding( .padding(
horizontal = WidgetDetailsDimensions.horizontalPadding, horizontal = WidgetDetailsDimensions.horizontalPadding,
vertical = WidgetDetailsDimensions.verticalPadding vertical = WidgetDetailsDimensions.verticalPadding,
) ),
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier modifier =
.clearAndSetSemantics { this.contentDescription = contentDescription } Modifier.clearAndSetSemantics { this.contentDescription = contentDescription }
.minimumInteractiveComponentSize() .minimumInteractiveComponentSize()
.graphicsLayer { alpha = detailsAlpha } .graphicsLayer { alpha = detailsAlpha }
.fillMaxSize() .fillMaxSize(),
) { ) {
WidgetLabel( WidgetLabel(label = widget.label, appIcon = appIcon, modifier = Modifier)
label = widget.label,
appIcon = appIcon,
modifier = Modifier
)
if (showAllDetails) { if (showAllDetails) {
WidgetSpanSizeLabel( WidgetSpanSizeLabel(spanX = widget.sizeInfo.spanX, spanY = widget.sizeInfo.spanY)
spanX = widget.sizeInfo.spanX,
spanY = widget.sizeInfo.spanY
)
widget.description?.let { WidgetDescription(it) } widget.description?.let { WidgetDescription(it) }
} }
} }
@@ -152,16 +146,12 @@ fun WidgetDetails(
visible = showAddButton, visible = showAddButton,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
enter = AddButtonDefaults.enterTransition, enter = AddButtonDefaults.enterTransition,
exit = AddButtonDefaults.exitTransition exit = AddButtonDefaults.exitTransition,
) { ) {
AddButton( AddButton(
widget = widget, widget = widget,
onClick = { onClick = {
onWidgetAddClick( onWidgetAddClick(WidgetInteractionInfo.WidgetAddInfo(widget.widgetInfo))
WidgetInteractionInfo.WidgetAddInfo(
widget.appWidgetProviderInfo
)
)
haptic.performHapticFeedback(HapticFeedbackType.Confirm) haptic.performHapticFeedback(HapticFeedbackType.Confirm)
}, },
) )
@@ -170,33 +160,28 @@ fun WidgetDetails(
} }
@Composable @Composable
private fun AddButton( private fun AddButton(widget: PickableWidget, onClick: () -> Unit) {
widget: PickableWidget,
onClick: () -> Unit,
) {
val accessibleDescription = val accessibleDescription =
stringResource(R.string.widget_tap_to_add_button_content_description, widget.label) stringResource(R.string.widget_tap_to_add_button_content_description, widget.label)
Box( Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button( Button(
modifier = Modifier.minimumInteractiveComponentSize(), modifier = Modifier.minimumInteractiveComponentSize(),
contentPadding = AddButtonDimensions.paddingValues, contentPadding = AddButtonDimensions.paddingValues,
colors = ButtonDefaults.buttonColors( colors =
containerColor = WidgetPickerTheme.colors.addButtonBackground, ButtonDefaults.buttonColors(
contentColor = WidgetPickerTheme.colors.addButtonContent containerColor = WidgetPickerTheme.colors.addButtonBackground,
), contentColor = WidgetPickerTheme.colors.addButtonContent,
),
onClick = onClick, onClick = onClick,
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Add, imageVector = Icons.Filled.Add,
contentDescription = null // decorative contentDescription = null, // decorative
) )
Text( Text(
modifier = Modifier.semantics { this.contentDescription = accessibleDescription }, 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( Row(
modifier = modifier, modifier = modifier,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
if (appIcon != null) { if (appIcon != null) {
appIcon() appIcon()
Spacer( Spacer(
modifier = modifier =
Modifier Modifier.width(WidgetDetailsDimensions.appIconLabelSpacing).fillMaxHeight()
.width(WidgetDetailsDimensions.appIconLabelSpacing)
.fillMaxHeight()
) )
} }
Text( Text(
@@ -272,12 +255,7 @@ private object WidgetDetailsDimensions {
} }
private object AddButtonDimensions { private object AddButtonDimensions {
val paddingValues = PaddingValues( val paddingValues = PaddingValues(start = 8.dp, top = 11.dp, end = 16.dp, bottom = 11.dp)
start = 8.dp,
top = 11.dp,
end = 16.dp,
bottom = 11.dp
)
} }
private object AddButtonDefaults { private object AddButtonDefaults {
@@ -63,8 +63,10 @@ import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import com.android.launcher3.widgetpicker.shared.model.WidgetId 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.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo 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.WidgetInteractionInfo
import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerTheme import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerTheme
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -75,11 +77,11 @@ fun WidgetPreview(
id: WidgetId, id: WidgetId,
sizeInfo: WidgetSizeInfo, sizeInfo: WidgetSizeInfo,
preview: WidgetPreview, preview: WidgetPreview,
appwidgetInfo: AppWidgetProviderInfo, widgetInfo: WidgetInfo,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showDragShadow: Boolean, showDragShadow: Boolean,
onWidgetInteraction: (WidgetInteractionInfo) -> Unit, onWidgetInteraction: (WidgetInteractionInfo) -> Unit,
onAddButtonToggle: (WidgetId) -> Unit onAddButtonToggle: (WidgetId) -> Unit,
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
@@ -93,16 +95,16 @@ fun WidgetPreview(
} }
Box( Box(
modifier = modifier modifier =
.wrapContentSize() modifier.wrapContentSize().clickable(
.clickable(
interactionSource = interactionSource, interactionSource = interactionSource,
// no ripples for preview taps that toggle the add button. // no ripples for preview taps that toggle the add button.
indication = null indication = null,
) { ) {
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey) haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
onAddButtonToggle(id) onAddButtonToggle(id)
}) { }
) {
when (preview) { when (preview) {
is WidgetPreview.PlaceholderWidgetPreview -> is WidgetPreview.PlaceholderWidgetPreview ->
PlaceholderWidgetPreview(size = containerSize, widgetRadius = widgetRadius) PlaceholderWidgetPreview(size = containerSize, widgetRadius = widgetRadius)
@@ -112,30 +114,34 @@ fun WidgetPreview(
bitmap = preview.bitmap, bitmap = preview.bitmap,
size = containerSize, size = containerSize,
widgetRadius = widgetRadius, widgetRadius = widgetRadius,
widgetInfo = appwidgetInfo, widgetInfo = widgetInfo,
showDragShadow = showDragShadow, showDragShadow = showDragShadow,
onWidgetInteraction = onWidgetInteraction, onWidgetInteraction = onWidgetInteraction,
) )
is WidgetPreview.RemoteViewsWidgetPreview -> is WidgetPreview.RemoteViewsWidgetPreview -> {
check(widgetInfo.isAppWidget())
RemoteViewsWidgetPreview( RemoteViewsWidgetPreview(
remoteViews = preview.remoteViews, remoteViews = preview.remoteViews,
widgetInfo = appwidgetInfo, widgetInfo = widgetInfo,
sizeInfo = sizeInfo, sizeInfo = sizeInfo,
widgetRadius = widgetRadius, widgetRadius = widgetRadius,
showDragShadow = showDragShadow, showDragShadow = showDragShadow,
onWidgetInteraction = onWidgetInteraction, onWidgetInteraction = onWidgetInteraction,
) )
}
is WidgetPreview.ProviderInfoWidgetPreview -> is WidgetPreview.ProviderInfoWidgetPreview -> {
check(widgetInfo.isAppWidget())
RemoteViewsWidgetPreview( RemoteViewsWidgetPreview(
previewLayoutProviderInfo = preview.providerInfo, previewLayoutProviderInfo = preview.providerInfo,
widgetInfo = appwidgetInfo, widgetInfo = widgetInfo,
sizeInfo = sizeInfo, sizeInfo = sizeInfo,
widgetRadius = widgetRadius, widgetRadius = widgetRadius,
showDragShadow = showDragShadow, showDragShadow = showDragShadow,
onWidgetInteraction = onWidgetInteraction, onWidgetInteraction = onWidgetInteraction,
) )
}
} }
} }
} }
@@ -145,8 +151,7 @@ private fun PlaceholderWidgetPreview(size: DpSize, widgetRadius: Dp) {
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = modifier =
Modifier Modifier.width(size.width)
.width(size.width)
.height(size.height) .height(size.height)
.background( .background(
color = WidgetPickerTheme.colors.widgetPlaceholderBackground, color = WidgetPickerTheme.colors.widgetPlaceholderBackground,
@@ -161,7 +166,7 @@ private fun PlaceholderWidgetPreview(size: DpSize, widgetRadius: Dp) {
private fun BitmapWidgetPreview( private fun BitmapWidgetPreview(
bitmap: Bitmap, bitmap: Bitmap,
size: DpSize, size: DpSize,
widgetInfo: AppWidgetProviderInfo, widgetInfo: WidgetInfo,
widgetRadius: Dp, widgetRadius: Dp,
showDragShadow: Boolean, showDragShadow: Boolean,
onWidgetInteraction: (WidgetInteractionInfo) -> Unit, onWidgetInteraction: (WidgetInteractionInfo) -> Unit,
@@ -170,22 +175,24 @@ private fun BitmapWidgetPreview(
val density = LocalDensity.current val density = LocalDensity.current
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val scaledBitmapDimensions by remember(bitmap, density, size) { val scaledBitmapDimensions by
derivedStateOf { bitmap.calculateScaledDimensions(density, size, widgetRadius) } remember(bitmap, density, size) {
} derivedStateOf { bitmap.calculateScaledDimensions(density, size, widgetRadius) }
}
val dragState by remember(widgetInfo, showDragShadow) {
derivedStateOf { val dragState by
DragState( remember(widgetInfo, showDragShadow) {
widgetInfo, derivedStateOf {
if (showDragShadow) { DragState(
ImageBitmapDragShadowBuilder(context, bitmap, scaledBitmapDimensions) widgetInfo,
} else { if (showDragShadow) {
TransparentDragShadowBuilder ImageBitmapDragShadowBuilder(context, bitmap, scaledBitmapDimensions)
} } else {
) TransparentDragShadowBuilder
},
)
}
} }
}
var imagePositionInParent by remember { mutableStateOf(Offset.Zero) } var imagePositionInParent by remember { mutableStateOf(Offset.Zero) }
@@ -199,8 +206,7 @@ private fun BitmapWidgetPreview(
contentDescription = null, // only visual (widget details provides the readable info) contentDescription = null, // only visual (widget details provides the readable info)
contentScale = ContentScale.FillBounds, contentScale = ContentScale.FillBounds,
modifier = modifier =
Modifier Modifier.onGloballyPositioned { coordinates ->
.onGloballyPositioned { coordinates ->
imagePositionInParent = coordinates.positionInParent() imagePositionInParent = coordinates.positionInParent()
} }
.pointerInput(bitmap) { .pointerInput(bitmap) {
@@ -215,21 +221,19 @@ private fun BitmapWidgetPreview(
calculateImageDragBounds( calculateImageDragBounds(
scaledBitmapDimensions = scaledBitmapDimensions, scaledBitmapDimensions = scaledBitmapDimensions,
imagePositionInParent = imagePositionInParent, imagePositionInParent = imagePositionInParent,
offset = offset offset = offset,
) )
onWidgetInteraction( onWidgetInteraction(
WidgetInteractionInfo.WidgetDragInfo( WidgetInteractionInfo.WidgetDragInfo(
mimeType = dragState.pickerMimeType, mimeType = dragState.pickerMimeType,
providerInfo = widgetInfo, widgetInfo = widgetInfo,
bounds = bounds, bounds = bounds,
widthPx = scaledBitmapDimensions.scaledSizePx.width, widthPx = scaledBitmapDimensions.scaledSizePx.width,
heightPx = scaledBitmapDimensions.scaledSizePx.height, heightPx = scaledBitmapDimensions.scaledSizePx.height,
previewInfo = WidgetPreview.BitmapWidgetPreview( previewInfo = WidgetPreview.BitmapWidgetPreview(bitmap = bitmap),
bitmap = bitmap,
),
) )
) )
} },
) )
} }
.width(scaledBitmapDimensions.scaledSizeDp.width) .width(scaledBitmapDimensions.scaledSizeDp.width)
@@ -242,26 +246,20 @@ private fun BitmapWidgetPreview(
private fun calculateImageDragBounds( private fun calculateImageDragBounds(
scaledBitmapDimensions: ImageScaledDimensions, scaledBitmapDimensions: ImageScaledDimensions,
imagePositionInParent: Offset, imagePositionInParent: Offset,
offset: Offset offset: Offset,
): Rect { ): Rect {
val bounds = Rect() val bounds = Rect()
bounds.left = 0 bounds.left = 0
bounds.top = 0 bounds.top = 0
bounds.right = scaledBitmapDimensions.scaledSizePx.width bounds.right = scaledBitmapDimensions.scaledSizePx.width
bounds.bottom = scaledBitmapDimensions.scaledSizePx.height bounds.bottom = scaledBitmapDimensions.scaledSizePx.height
val xOffset: Int = val xOffset: Int = (imagePositionInParent.x - offset.x).roundToInt()
(imagePositionInParent.x - offset.x).roundToInt() val yOffset: Int = (imagePositionInParent.y - offset.y).roundToInt()
val yOffset: Int =
(imagePositionInParent.y - offset.y).roundToInt()
bounds.offset(xOffset, yOffset) bounds.offset(xOffset, yOffset)
return bounds return bounds
} }
private fun Bitmap.calculateScaledDimensions( private fun Bitmap.calculateScaledDimensions(density: Density, size: DpSize, widgetRadius: Dp) =
density: Density,
size: DpSize,
widgetRadius: Dp
) =
with(density) { with(density) {
val bitmapSize = DpSize(width = width.toDp(), height = height.toDp()) val bitmapSize = DpSize(width = width.toDp(), height = height.toDp())
val bitmapAspectRatio = bitmapSize.width / bitmapSize.height 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 // Scale by width if image has larger aspect ratio than the container else by
// height; and avoid cropping the previews. // height; and avoid cropping the previews.
val scale = if (bitmapAspectRatio > containerAspectRatio) { val scale =
size.width / bitmapSize.width if (bitmapAspectRatio > containerAspectRatio) {
} else { size.width / bitmapSize.width
size.height / bitmapSize.height } else {
} size.height / bitmapSize.height
}
val scaledDpSize = DpSize( val scaledDpSize =
width = bitmapSize.width * scale, DpSize(width = bitmapSize.width * scale, height = bitmapSize.height * scale)
height = bitmapSize.height * scale val scaledPxSize =
) IntSize(
val scaledPxSize = IntSize( width = scaledDpSize.width.roundToPx(),
width = scaledDpSize.width.roundToPx(), height = scaledDpSize.height.roundToPx(),
height = scaledDpSize.height.roundToPx() )
)
val scaledRadius = (widgetRadius * scale).coerceAtMost(widgetRadius).value.roundToInt().dp val scaledRadius = (widgetRadius * scale).coerceAtMost(widgetRadius).value.roundToInt().dp
ImageScaledDimensions( ImageScaledDimensions(
@@ -290,7 +288,7 @@ private fun Bitmap.calculateScaledDimensions(
scaledSizePx = scaledPxSize, scaledSizePx = scaledPxSize,
scaledSizeDp = scaledDpSize, scaledSizeDp = scaledDpSize,
scaledRadiusDp = scaledRadius, scaledRadiusDp = scaledRadius,
scaledRadiusPx = scaledRadius.toPx() scaledRadiusPx = scaledRadius.toPx(),
) )
} }
@@ -298,7 +296,7 @@ private fun Bitmap.calculateScaledDimensions(
private fun RemoteViewsWidgetPreview( private fun RemoteViewsWidgetPreview(
remoteViews: RemoteViews? = null, remoteViews: RemoteViews? = null,
previewLayoutProviderInfo: AppWidgetProviderInfo? = null, previewLayoutProviderInfo: AppWidgetProviderInfo? = null,
widgetInfo: AppWidgetProviderInfo, widgetInfo: WidgetInfo.AppWidgetInfo,
sizeInfo: WidgetSizeInfo, sizeInfo: WidgetSizeInfo,
widgetRadius: Dp, widgetRadius: Dp,
onWidgetInteraction: (WidgetInteractionInfo) -> Unit, onWidgetInteraction: (WidgetInteractionInfo) -> Unit,
@@ -308,67 +306,71 @@ private fun RemoteViewsWidgetPreview(
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val appWidgetHostView by val appWidgetHostView by
remember(sizeInfo, widgetInfo) { remember(sizeInfo, widgetInfo) {
derivedStateOf { derivedStateOf {
WidgetPreviewHostView(context).apply { WidgetPreviewHostView(context).apply {
setContainerSizePx( setContainerSizePx(
IntSize(sizeInfo.containerWidthPx, sizeInfo.containerHeightPx) IntSize(sizeInfo.containerWidthPx, sizeInfo.containerHeightPx)
) )
}
} }
} }
}
val dragState by remember { val dragState by remember {
derivedStateOf { derivedStateOf {
DragState( DragState(
widgetInfo = widgetInfo, widgetInfo = widgetInfo,
dragShadowBuilder = if (showDragShadow) { dragShadowBuilder =
DragShadowBuilder(appWidgetHostView) if (showDragShadow) {
} else { DragShadowBuilder(appWidgetHostView)
TransparentDragShadowBuilder } else {
} TransparentDragShadowBuilder
},
) )
} }
} }
key(appWidgetHostView) { key(appWidgetHostView) {
AndroidView( AndroidView(
modifier = Modifier modifier =
.pointerInput(appWidgetHostView) { Modifier.pointerInput(appWidgetHostView) {
detectDragGesturesAfterLongPress( detectDragGesturesAfterLongPress(
onDrag = { change, _ -> change.consume() }, onDrag = { change, _ -> change.consume() },
onDragStart = { offset -> onDragStart = { offset ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
dragState.startDrag(appWidgetHostView) dragState.startDrag(appWidgetHostView)
onWidgetInteraction( onWidgetInteraction(
WidgetInteractionInfo.WidgetDragInfo( WidgetInteractionInfo.WidgetDragInfo(
mimeType = dragState.pickerMimeType, mimeType = dragState.pickerMimeType,
providerInfo = widgetInfo, widgetInfo = widgetInfo,
bounds = appWidgetHostView.getDragBoundsForOffset(offset), bounds = appWidgetHostView.getDragBoundsForOffset(offset),
widthPx = appWidgetHostView.measuredWidth, widthPx = appWidgetHostView.measuredWidth,
heightPx = appWidgetHostView.measuredHeight, heightPx = appWidgetHostView.measuredHeight,
previewInfo = when { previewInfo =
remoteViews != null -> when {
WidgetPreview.RemoteViewsWidgetPreview( remoteViews != null ->
remoteViews = remoteViews, WidgetPreview.RemoteViewsWidgetPreview(
) remoteViews = remoteViews
)
previewLayoutProviderInfo != null -> previewLayoutProviderInfo != null ->
WidgetPreview.ProviderInfoWidgetPreview( WidgetPreview.ProviderInfoWidgetPreview(
providerInfo = previewLayoutProviderInfo providerInfo = previewLayoutProviderInfo
) )
else -> else ->
throw IllegalStateException("No preview during drag") throw IllegalStateException(
} "No preview during drag"
)
},
)
) )
) },
}, )
) }
} .wrapContentSize()
.wrapContentSize() .clip(RoundedCornerShape(widgetRadius)),
.clip(RoundedCornerShape(widgetRadius)),
factory = { appWidgetHostView }, factory = { appWidgetHostView },
update = { view -> update = { view ->
// if preview.remoteViews is null, initial layout will render. // if preview.remoteViews is null, initial layout will render.
@@ -376,7 +378,7 @@ private fun RemoteViewsWidgetPreview(
// to be the previewLayout. // to be the previewLayout.
view.setAppWidget( view.setAppWidget(
/*appWidgetId=*/ NO_OP_APP_WIDGET_ID, /*appWidgetId=*/ NO_OP_APP_WIDGET_ID,
/*info=*/ previewLayoutProviderInfo ?: widgetInfo, /*info=*/ previewLayoutProviderInfo ?: widgetInfo.appWidgetProviderInfo,
) )
view.updateAppWidget(remoteViews) 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 * @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) * (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 * @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 * shadow containing the preview; if not set, a transparent shadow is rendered and host should
* manage providing a shadow on its own. * manage providing a shadow on its own.
* @param onWidgetInteraction callback invoked when a widget is being dragged and picker has started * @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 * @param modifier modifier with parent constraints and additional modifications
*/ */
@Composable @Composable
@@ -93,12 +93,13 @@ fun WidgetsGrid(
addButtonWidgetId = addButtonWidgetId, addButtonWidgetId = addButtonWidgetId,
onWidgetInteraction = onWidgetInteraction, onWidgetInteraction = onWidgetInteraction,
onAddButtonToggle = { id -> onAddButtonToggle = { id ->
addButtonWidgetId = if (id != addButtonWidgetId) { addButtonWidgetId =
id if (id != addButtonWidgetId) {
} else { id
null } else {
} null
} }
},
) )
} }
} }
@@ -155,7 +156,7 @@ private fun WidgetsFlowRow(
appIcons = appIcons, appIcons = appIcons,
addButtonWidgetId = addButtonWidgetId, addButtonWidgetId = addButtonWidgetId,
onWidgetInteraction = onWidgetInteraction, onWidgetInteraction = onWidgetInteraction,
onAddButtonToggle = onAddButtonToggle onAddButtonToggle = onAddButtonToggle,
) )
}, },
previewContainerWidthPx = widgetSizeGroup.previewContainerWidthPx, previewContainerWidthPx = widgetSizeGroup.previewContainerWidthPx,
@@ -184,21 +185,19 @@ private fun Previews(
Box( Box(
contentAlignment = Alignment.BottomCenter, contentAlignment = Alignment.BottomCenter,
modifier = modifier =
Modifier Modifier.fillMaxSize().clearAndSetSemantics {
.fillMaxSize() traversalIndex = index.toFloat()
.clearAndSetSemantics { testTag = buildWidgetPickerTestTag(WIDGET_PREVIEW_TEST_TAG)
traversalIndex = index.toFloat() },
testTag = buildWidgetPickerTestTag(WIDGET_PREVIEW_TEST_TAG)
},
) { ) {
WidgetPreview( WidgetPreview(
id = widgetItem.id, id = widgetItem.id,
sizeInfo = widgetItem.sizeInfo, sizeInfo = widgetItem.sizeInfo,
preview = widgetPreview, preview = widgetPreview,
appwidgetInfo = widgetItem.appWidgetProviderInfo, widgetInfo = widgetItem.widgetInfo,
showDragShadow = showDragShadow, showDragShadow = showDragShadow,
onWidgetInteraction = onWidgetInteraction, onWidgetInteraction = onWidgetInteraction,
onAddButtonToggle = onAddButtonToggle onAddButtonToggle = onAddButtonToggle,
) )
} }
} }
@@ -211,7 +210,7 @@ private fun Details(
addButtonWidgetId: WidgetId?, addButtonWidgetId: WidgetId?,
appIcons: Map<WidgetAppId, WidgetAppIcon>, appIcons: Map<WidgetAppId, WidgetAppIcon>,
onWidgetInteraction: (WidgetInteractionInfo) -> Unit, onWidgetInteraction: (WidgetInteractionInfo) -> Unit,
onAddButtonToggle: (WidgetId) -> Unit onAddButtonToggle: (WidgetId) -> Unit,
) { ) {
widgets.forEachIndexed { index, widgetItem -> widgets.forEachIndexed { index, widgetItem ->
val appId = widgetItem.appId val appId = widgetItem.appId
@@ -376,8 +375,8 @@ private fun Placeable.PlacementScope.placeRows(
// Move to next row // Move to next row
yPosition += yPosition +=
measuredRow.tallestPreviewHeight + measuredRow.tallestPreviewHeight +
measuredRow.tallestDetailsHeight + measuredRow.tallestDetailsHeight +
rowVerticalSpacingPx 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.PickableWidget
import com.android.launcher3.widgetpicker.shared.model.WidgetAppId import com.android.launcher3.widgetpicker.shared.model.WidgetAppId
import com.android.launcher3.widgetpicker.shared.model.WidgetId 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.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo
import com.android.launcher3.widgetpicker.tests.R import com.android.launcher3.widgetpicker.tests.R
@@ -253,7 +254,7 @@ object WidgetsGridTestSamples {
containerWidthPx = cellWidth, containerWidthPx = cellWidth,
containerHeightPx = cellHeight, containerHeightPx = cellHeight,
), ),
appWidgetProviderInfo = newAppWidgetInfo("OneByOneProvider"), widgetInfo = WidgetInfo.AppWidgetInfo(newAppWidgetInfo("OneByOneProvider")),
) )
private fun twoByTwo(cellWidth: Int, cellHeight: Int) = private fun twoByTwo(cellWidth: Int, cellHeight: Int) =
@@ -262,7 +263,7 @@ object WidgetsGridTestSamples {
appId = TEST_WIDGET_APP_ID, appId = TEST_WIDGET_APP_ID,
label = "Two by Two", label = "Two by Two",
description = null, description = null,
appWidgetProviderInfo = newAppWidgetInfo("TwoByTwoProvider"), widgetInfo = WidgetInfo.AppWidgetInfo(newAppWidgetInfo("TwoByTwoProvider")),
sizeInfo = sizeInfo =
WidgetSizeInfo( WidgetSizeInfo(
spanX = 2, spanX = 2,
@@ -283,7 +284,7 @@ object WidgetsGridTestSamples {
appId = TEST_WIDGET_APP_ID, appId = TEST_WIDGET_APP_ID,
label = "Three by two", label = "Three by two",
description = null, description = null,
appWidgetProviderInfo = newAppWidgetInfo("threeByTwoProvider"), widgetInfo = WidgetInfo.AppWidgetInfo(newAppWidgetInfo("threeByTwoProvider")),
sizeInfo = sizeInfo =
WidgetSizeInfo( WidgetSizeInfo(
spanX = 3, spanX = 3,
@@ -304,7 +305,7 @@ object WidgetsGridTestSamples {
appId = TEST_WIDGET_APP_ID, appId = TEST_WIDGET_APP_ID,
label = "Four by two", label = "Four by two",
description = null, description = null,
appWidgetProviderInfo = newAppWidgetInfo("FourByTwoProvider"), widgetInfo = WidgetInfo.AppWidgetInfo(newAppWidgetInfo("FourByTwoProvider")),
sizeInfo = sizeInfo =
WidgetSizeInfo( WidgetSizeInfo(
spanX = 4, 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.WidgetApp
import com.android.launcher3.widgetpicker.shared.model.WidgetAppId import com.android.launcher3.widgetpicker.shared.model.WidgetAppId
import com.android.launcher3.widgetpicker.shared.model.WidgetId 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.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo
import com.android.launcher3.widgetpicker.shared.model.WidgetUserProfile import com.android.launcher3.widgetpicker.shared.model.WidgetUserProfile
@@ -118,10 +119,13 @@ object TestUtils {
appId = finalWidgetAppId, appId = finalWidgetAppId,
label = providerClassName, label = providerClassName,
description = null, description = null,
appWidgetProviderInfo = AppWidgetProviderInfo().apply { widgetInfo =
widgetCategory = category WidgetInfo.AppWidgetInfo(
provider = ComponentName.createRelative(PACKAGE_NAME, providerClassName) AppWidgetProviderInfo().apply {
}, widgetCategory = category
provider = ComponentName.createRelative(PACKAGE_NAME, providerClassName)
}
),
sizeInfo = sizeInfo =
WidgetSizeInfo( WidgetSizeInfo(
spanX = 2, spanX = 2,
@@ -53,11 +53,9 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@AllowedDevices(allowed = [DeviceProduct.CF_PHONE]) @AllowedDevices(allowed = [DeviceProduct.CF_PHONE])
class WidgetInteractionsTest { class WidgetInteractionsTest {
@get:Rule @get:Rule val limitDevicesRule = LimitDevicesRule()
val limitDevicesRule = LimitDevicesRule()
@get:Rule @get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun tapPreview_andClickAdd() { fun tapPreview_andClickAdd() {
@@ -66,7 +64,8 @@ class WidgetInteractionsTest {
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
// tap on preview for widget 1 // tap on preview for widget 1
composeTestRule.onAllNodesWithTag(PREVIEW_TEST_TAG) composeTestRule
.onAllNodesWithTag(PREVIEW_TEST_TAG)
.assertCountEquals(2) .assertCountEquals(2)
.onFirst() .onFirst()
.performClick() .performClick()
@@ -74,18 +73,19 @@ class WidgetInteractionsTest {
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
composeTestRule.onNodeWithText(WIDGET_ONE.label).isNotDisplayed() // label text not shown composeTestRule.onNodeWithText(WIDGET_ONE.label).isNotDisplayed() // label text not shown
composeTestRule.onAllNodesWithText(ADD_BUTTON_TEXT).assertCountEquals(1) composeTestRule.onAllNodesWithText(ADD_BUTTON_TEXT).assertCountEquals(1)
composeTestRule.onNodeWithContentDescription(WIDGET_ONE_ADD_BUTTON_CONTENT_DESC) composeTestRule
.onNodeWithContentDescription(WIDGET_ONE_ADD_BUTTON_CONTENT_DESC)
.assertExists() .assertExists()
.performClick() .performClick()
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
// widget interaction callback invoked and correct provider info returned. // widget interaction callback invoked and correct provider info returned.
composeTestRule.onNodeWithText(WIDGET_ONE.appWidgetProviderInfo.provider.toString()) composeTestRule.onNodeWithText(WIDGET_ONE.widgetInfo.toString()).assertExists()
.assertExists()
// tap again on preview for widget 1 // tap again on preview for widget 1
composeTestRule.onAllNodesWithTag(PREVIEW_TEST_TAG) composeTestRule
.onAllNodesWithTag(PREVIEW_TEST_TAG)
.assertCountEquals(2) .assertCountEquals(2)
.onFirst() .onFirst()
.performClick() .performClick()
@@ -103,7 +103,8 @@ class WidgetInteractionsTest {
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
// tap on preview for widget 1 // tap on preview for widget 1
composeTestRule.onAllNodesWithTag(PREVIEW_TEST_TAG) composeTestRule
.onAllNodesWithTag(PREVIEW_TEST_TAG)
.assertCountEquals(2) .assertCountEquals(2)
.onFirst() .onFirst()
.performClick() .performClick()
@@ -114,11 +115,13 @@ class WidgetInteractionsTest {
composeTestRule.onNodeWithText(WIDGET_ONE.label).isNotDisplayed() composeTestRule.onNodeWithText(WIDGET_ONE.label).isNotDisplayed()
composeTestRule.onNodeWithText(WIDGET_TWO.label).isNotDisplayed() composeTestRule.onNodeWithText(WIDGET_TWO.label).isNotDisplayed()
composeTestRule.onAllNodesWithText(ADD_BUTTON_TEXT).assertCountEquals(1) composeTestRule.onAllNodesWithText(ADD_BUTTON_TEXT).assertCountEquals(1)
composeTestRule.onNodeWithContentDescription(WIDGET_ONE_ADD_BUTTON_CONTENT_DESC) composeTestRule
.onNodeWithContentDescription(WIDGET_ONE_ADD_BUTTON_CONTENT_DESC)
.assertExists() .assertExists()
// tap on preview for widget 2 // tap on preview for widget 2
composeTestRule.onAllNodesWithTag(PREVIEW_TEST_TAG) composeTestRule
.onAllNodesWithTag(PREVIEW_TEST_TAG)
.assertCountEquals(2) .assertCountEquals(2)
.onLast() .onLast()
.performClick() .performClick()
@@ -129,11 +132,11 @@ class WidgetInteractionsTest {
composeTestRule.onNodeWithText(WIDGET_ONE.label).isDisplayed() composeTestRule.onNodeWithText(WIDGET_ONE.label).isDisplayed()
composeTestRule.onNodeWithText(WIDGET_TWO.label).isNotDisplayed() composeTestRule.onNodeWithText(WIDGET_TWO.label).isNotDisplayed()
composeTestRule.onAllNodesWithText(ADD_BUTTON_TEXT).assertCountEquals(1) composeTestRule.onAllNodesWithText(ADD_BUTTON_TEXT).assertCountEquals(1)
composeTestRule.onNodeWithContentDescription(WIDGET_TWO_ADD_BUTTON_CONTENT_DESC) composeTestRule
.onNodeWithContentDescription(WIDGET_TWO_ADD_BUTTON_CONTENT_DESC)
.assertExists() .assertExists()
} }
@Composable @Composable
fun TapToAddTestComposable() { fun TapToAddTestComposable() {
var provider by remember { mutableStateOf("invalid") } var provider by remember { mutableStateOf("invalid") }
@@ -149,7 +152,7 @@ class WidgetInteractionsTest {
showDragShadow = false, showDragShadow = false,
onWidgetInteraction = { widgetInteractionInfo -> onWidgetInteraction = { widgetInteractionInfo ->
if (widgetInteractionInfo is WidgetInteractionInfo.WidgetAddInfo) { 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_ONE = PERSONAL_TEST_APPS[0].widgets[0]
private val WIDGET_TWO = PERSONAL_TEST_APPS[1].widgets[0] private val WIDGET_TWO = PERSONAL_TEST_APPS[1].widgets[0]
private val TEST_WIDGET_GROUP = WidgetSizeGroup( private val TEST_WIDGET_GROUP =
previewContainerHeightPx = 200, WidgetSizeGroup(
previewContainerWidthPx = 200, previewContainerHeightPx = 200,
widgets = listOf(WIDGET_ONE, WIDGET_TWO) previewContainerWidthPx = 200,
) widgets = listOf(WIDGET_ONE, WIDGET_TWO),
)
private val PREVIEWS = mapOf( private val PREVIEWS =
WIDGET_ONE.id to TestUtils.createBitmapPreview(), mapOf(
WIDGET_TWO.id to TestUtils.createBitmapPreview() WIDGET_ONE.id to TestUtils.createBitmapPreview(),
) WIDGET_TWO.id to TestUtils.createBitmapPreview(),
)
private val PREVIEW_TEST_TAG = buildWidgetPickerTestTag("widget_preview") private val PREVIEW_TEST_TAG = buildWidgetPickerTestTag("widget_preview")
private const val ADD_BUTTON_TEXT = "Add" private const val ADD_BUTTON_TEXT = "Add"
-7
View File
@@ -139,13 +139,6 @@
<string-array name="filtered_components" ></string-array> <string-array name="filtered_components" ></string-array>
<string-array name="default_featured_widget_apps" translatable="false"> <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> </string-array>
<!-- Swipe back to home related --> <!-- 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.LauncherAnimUtils.VIEW_ALPHA;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; 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.icons.FastBitmapDrawable.getDisabledColorFilter;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 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 // When widgets are dropped from another window, we don't want to remove the
// dragView on resume of launcher. // dragView on resume of launcher.
if (Flags.enableWidgetPickerRefactor() 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); dragLayer.removeView(child);
} }
} }
@@ -134,7 +134,7 @@ public abstract class ShortcutConfigActivityInfo implements CachedObject {
@TargetApi(26) @TargetApi(26)
public static class ShortcutConfigActivityInfoVO extends ShortcutConfigActivityInfo { public static class ShortcutConfigActivityInfoVO extends ShortcutConfigActivityInfo {
private final LauncherActivityInfo mInfo; public final LauncherActivityInfo mInfo;
public ShortcutConfigActivityInfoVO(LauncherActivityInfo info) { public ShortcutConfigActivityInfoVO(LauncherActivityInfo info) {
super(info.getComponentName(), info.getUser(), 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.CancellableTask;
import com.android.launcher3.util.Executors; import com.android.launcher3.util.Executors;
import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.util.WidgetSizes; import com.android.launcher3.widget.util.WidgetSizes;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@@ -272,8 +271,7 @@ public class DatabaseWidgetPreviewLoader {
private Bitmap generateShortcutPreview( private Bitmap generateShortcutPreview(
ShortcutConfigActivityInfo info, int maxWidth, int maxHeight) { ShortcutConfigActivityInfo info, int maxWidth, int maxHeight) {
int iconSize = ActivityContext.lookupContext( int iconSize = mDeviceProfile.getAllAppsProfile().getIconSizePx();
mContext).getDeviceProfile().getAllAppsProfile().getIconSizePx();
int padding = mContext.getResources() int padding = mContext.getResources()
.getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding); .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);