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