2025-05-29 13:26:00 -07:00
|
|
|
/*
|
|
|
|
|
* Copyright (C) 2025 The Android Open Source Project
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
|
*
|
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
*
|
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
package com.android.launcher3.widgetpicker
|
|
|
|
|
|
2025-05-30 08:13:45 -07:00
|
|
|
import android.app.Activity.RESULT_OK
|
2025-05-29 13:26:00 -07:00
|
|
|
import android.content.Context
|
2025-05-30 08:13:45 -07:00
|
|
|
import android.content.Intent
|
2025-06-08 17:32:28 +00:00
|
|
|
import androidx.compose.foundation.isSystemInDarkTheme
|
2025-05-29 13:26:00 -07:00
|
|
|
import androidx.compose.material3.MaterialTheme
|
|
|
|
|
import androidx.compose.runtime.DisposableEffect
|
|
|
|
|
import androidx.compose.runtime.remember
|
|
|
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
|
|
|
import androidx.compose.ui.platform.ComposeView
|
|
|
|
|
import androidx.compose.ui.platform.LocalView
|
2025-05-30 08:13:45 -07:00
|
|
|
import com.android.launcher3.Launcher
|
2025-05-29 13:26:00 -07:00
|
|
|
import com.android.launcher3.R
|
|
|
|
|
import com.android.launcher3.compose.ComposeFacade
|
|
|
|
|
import com.android.launcher3.compose.core.widgetpicker.WidgetPickerComposeWrapper
|
|
|
|
|
import com.android.launcher3.concurrent.annotations.BackgroundContext
|
|
|
|
|
import com.android.launcher3.dagger.ApplicationContext
|
2025-05-30 08:13:45 -07:00
|
|
|
import com.android.launcher3.util.ApiWrapper
|
|
|
|
|
import com.android.launcher3.widgetpicker.WidgetPickerConfig.Companion.EXTRA_IS_PENDING_WIDGET_DRAG
|
2025-05-29 13:26:00 -07:00
|
|
|
import com.android.launcher3.widgetpicker.data.repository.WidgetAppIconsRepository
|
|
|
|
|
import com.android.launcher3.widgetpicker.data.repository.WidgetUsersRepository
|
|
|
|
|
import com.android.launcher3.widgetpicker.data.repository.WidgetsRepository
|
2025-05-30 08:13:45 -07:00
|
|
|
import com.android.launcher3.widgetpicker.listeners.WidgetPickerAddItemListener
|
|
|
|
|
import com.android.launcher3.widgetpicker.listeners.WidgetPickerDragItemListener
|
2025-06-02 22:34:00 -07:00
|
|
|
import com.android.launcher3.widgetpicker.shared.model.HostConstraint
|
2025-05-29 13:26:00 -07:00
|
|
|
import com.android.launcher3.widgetpicker.shared.model.WidgetHostInfo
|
2025-06-08 17:32:28 +00:00
|
|
|
import com.android.launcher3.widgetpicker.shared.model.isAppWidget
|
|
|
|
|
import com.android.launcher3.widgetpicker.theme.darkWidgetPickerColors
|
|
|
|
|
import com.android.launcher3.widgetpicker.theme.lightWidgetPickerColors
|
2025-05-30 08:13:45 -07:00
|
|
|
import com.android.launcher3.widgetpicker.ui.WidgetInteractionInfo
|
|
|
|
|
import com.android.launcher3.widgetpicker.ui.WidgetPickerEventListeners
|
2025-06-02 21:36:06 -07:00
|
|
|
import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerTheme
|
2025-05-29 13:26:00 -07:00
|
|
|
import javax.inject.Inject
|
|
|
|
|
import javax.inject.Provider
|
|
|
|
|
import kotlin.coroutines.CoroutineContext
|
2025-06-08 17:32:28 +00:00
|
|
|
import kotlinx.coroutines.launch
|
2025-05-29 13:26:00 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* An helper that bootstraps widget picker UI (from [WidgetPickerComponent]) in to
|
2025-05-30 08:13:45 -07:00
|
|
|
* [WidgetPickerActivity] when compose is available and widget picker refactor flags are on.
|
2025-05-29 13:26:00 -07:00
|
|
|
*
|
|
|
|
|
* Sets up the bindings necessary for widget picker component.
|
|
|
|
|
*/
|
2025-06-08 17:32:28 +00:00
|
|
|
class WidgetPickerComposeWrapperImpl
|
|
|
|
|
@Inject
|
|
|
|
|
constructor(
|
2025-05-29 13:26:00 -07:00
|
|
|
private val widgetPickerComponentProvider: Provider<WidgetPickerComponent.Factory>,
|
|
|
|
|
private val widgetsRepository: WidgetsRepository,
|
|
|
|
|
private val widgetUsersRepository: WidgetUsersRepository,
|
|
|
|
|
private val widgetAppIconsRepository: WidgetAppIconsRepository,
|
2025-06-08 17:32:28 +00:00
|
|
|
@BackgroundContext private val backgroundContext: CoroutineContext,
|
|
|
|
|
@ApplicationContext private val appContext: Context,
|
2025-05-30 08:13:45 -07:00
|
|
|
private val apiWrapper: ApiWrapper,
|
2025-05-29 13:26:00 -07:00
|
|
|
) : WidgetPickerComposeWrapper {
|
2025-05-30 08:13:45 -07:00
|
|
|
|
2025-05-29 13:26:00 -07:00
|
|
|
override fun showAllWidgets(
|
|
|
|
|
activity: WidgetPickerActivity,
|
2025-06-08 17:32:28 +00:00
|
|
|
widgetPickerConfig: WidgetPickerConfig,
|
2025-05-29 13:26:00 -07:00
|
|
|
) {
|
2025-05-30 08:13:45 -07:00
|
|
|
val widgetPickerComponent = newWidgetPickerComponent(widgetPickerConfig)
|
|
|
|
|
val callbacks = activity.buildEventListeners(widgetPickerConfig, apiWrapper)
|
2025-05-29 13:26:00 -07:00
|
|
|
|
|
|
|
|
val fullWidgetsCatalog = widgetPickerComponent.getFullWidgetsCatalog()
|
|
|
|
|
val composeView = ComposeFacade.initComposeView(activity.asContext()) as ComposeView
|
2025-05-30 08:13:45 -07:00
|
|
|
|
2025-05-29 13:26:00 -07:00
|
|
|
composeView.apply {
|
|
|
|
|
setContent {
|
|
|
|
|
val scope = rememberCoroutineScope()
|
|
|
|
|
val view = LocalView.current
|
|
|
|
|
|
2025-06-08 17:32:28 +00:00
|
|
|
val widgetPickerColors =
|
|
|
|
|
if (isSystemInDarkTheme()) {
|
|
|
|
|
darkWidgetPickerColors()
|
|
|
|
|
} else {
|
|
|
|
|
lightWidgetPickerColors()
|
|
|
|
|
}
|
2025-06-02 21:36:06 -07:00
|
|
|
|
2025-05-29 13:26:00 -07:00
|
|
|
MaterialTheme { // TODO(b/408283627): Use launcher theme.
|
2025-06-02 21:36:06 -07:00
|
|
|
WidgetPickerTheme(colors = widgetPickerColors) {
|
|
|
|
|
val eventListeners = remember { callbacks }
|
|
|
|
|
fullWidgetsCatalog.Content(eventListeners)
|
|
|
|
|
}
|
2025-05-29 13:26:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DisposableEffect(view) {
|
2025-06-08 17:32:28 +00:00
|
|
|
scope.launch { initializeRepositories() }
|
2025-05-29 13:26:00 -07:00
|
|
|
|
2025-06-08 17:32:28 +00:00
|
|
|
onDispose { cleanUpRepositories() }
|
2025-05-29 13:26:00 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-30 08:13:45 -07:00
|
|
|
checkNotNull(activity.dragLayer).addView(composeView)
|
2025-05-29 13:26:00 -07:00
|
|
|
}
|
|
|
|
|
|
2025-05-30 08:13:45 -07:00
|
|
|
private fun newWidgetPickerComponent(
|
|
|
|
|
widgetPickerConfig: WidgetPickerConfig
|
|
|
|
|
): WidgetPickerComponent {
|
2025-06-08 17:32:28 +00:00
|
|
|
return widgetPickerComponentProvider
|
|
|
|
|
.get()
|
2025-05-29 13:26:00 -07:00
|
|
|
.build(
|
|
|
|
|
widgetsRepository = widgetsRepository,
|
|
|
|
|
widgetUsersRepository = widgetUsersRepository,
|
|
|
|
|
widgetAppIconsRepository = widgetAppIconsRepository,
|
2025-06-08 17:32:28 +00:00
|
|
|
widgetHostInfo =
|
|
|
|
|
WidgetHostInfo(
|
|
|
|
|
title =
|
|
|
|
|
widgetPickerConfig.title
|
|
|
|
|
?: appContext.resources.getString(R.string.widget_button_text),
|
|
|
|
|
description = widgetPickerConfig.description,
|
|
|
|
|
constraints = widgetPickerConfig.asHostConstraints(),
|
|
|
|
|
showDragShadow = !widgetPickerConfig.isForHomeScreen,
|
|
|
|
|
),
|
|
|
|
|
backgroundContext = backgroundContext,
|
2025-05-29 13:26:00 -07:00
|
|
|
)
|
2025-05-30 08:13:45 -07:00
|
|
|
}
|
2025-05-29 13:26:00 -07:00
|
|
|
|
|
|
|
|
private fun initializeRepositories() {
|
|
|
|
|
widgetsRepository.initialize()
|
|
|
|
|
widgetUsersRepository.initialize()
|
|
|
|
|
widgetAppIconsRepository.initialize()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun cleanUpRepositories() {
|
|
|
|
|
widgetsRepository.cleanUp()
|
|
|
|
|
widgetUsersRepository.cleanUp()
|
|
|
|
|
widgetAppIconsRepository.cleanUp()
|
|
|
|
|
}
|
2025-05-30 08:13:45 -07:00
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
private const val HOME_SCREEN_WIDGET_INTERACTION_REASON_STRING =
|
|
|
|
|
"WidgetPickerActivity.OnWidgetInteraction"
|
|
|
|
|
|
|
|
|
|
private fun WidgetPickerActivity.buildEventListeners(
|
|
|
|
|
widgetPickerConfig: WidgetPickerConfig,
|
2025-06-08 17:32:28 +00:00
|
|
|
apiWrapper: ApiWrapper,
|
|
|
|
|
) =
|
|
|
|
|
object : WidgetPickerEventListeners {
|
|
|
|
|
override fun onClose() {
|
|
|
|
|
finish()
|
|
|
|
|
}
|
2025-05-30 08:13:45 -07:00
|
|
|
|
2025-06-08 17:32:28 +00:00
|
|
|
override fun onWidgetInteraction(widgetInteractionInfo: WidgetInteractionInfo) {
|
|
|
|
|
if (widgetPickerConfig.isForHomeScreen) {
|
|
|
|
|
handleWidgetInteractionForHomeScreen(widgetInteractionInfo, apiWrapper)
|
|
|
|
|
} else {
|
|
|
|
|
handleWidgetInteractionForExternalHost(widgetInteractionInfo)
|
|
|
|
|
}
|
2025-05-30 08:13:45 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles communication with the home screen about the "add" and "drag" interactions on
|
|
|
|
|
* widgets within widget picker.
|
|
|
|
|
*
|
|
|
|
|
* 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
|
2025-06-08 17:32:28 +00:00
|
|
|
* handles the drag until completion.
|
2025-05-30 08:13:45 -07:00
|
|
|
* - WidgetPickerAddItemListener: once launcher is shown, triggers the flow to add the
|
2025-06-08 17:32:28 +00:00
|
|
|
* widget to workspace.
|
2025-05-30 08:13:45 -07:00
|
|
|
*/
|
|
|
|
|
private fun WidgetPickerActivity.handleWidgetInteractionForHomeScreen(
|
|
|
|
|
interactionInfo: WidgetInteractionInfo,
|
2025-06-08 17:32:28 +00:00
|
|
|
apiWrapper: ApiWrapper,
|
2025-05-30 08:13:45 -07:00
|
|
|
) {
|
2025-06-08 17:32:28 +00:00
|
|
|
val interactionListener =
|
|
|
|
|
when (interactionInfo) {
|
|
|
|
|
is WidgetInteractionInfo.WidgetDragInfo ->
|
|
|
|
|
WidgetPickerDragItemListener(
|
|
|
|
|
mimeType = interactionInfo.mimeType,
|
|
|
|
|
widgetInfo = interactionInfo.widgetInfo,
|
|
|
|
|
widgetPreview = interactionInfo.previewInfo,
|
|
|
|
|
previewRect = interactionInfo.bounds,
|
|
|
|
|
previewWidth = interactionInfo.widthPx,
|
|
|
|
|
)
|
2025-05-30 08:13:45 -07:00
|
|
|
|
2025-06-08 17:32:28 +00:00
|
|
|
is WidgetInteractionInfo.WidgetAddInfo ->
|
|
|
|
|
WidgetPickerAddItemListener(interactionInfo.widgetInfo)
|
|
|
|
|
}
|
2025-05-30 08:13:45 -07:00
|
|
|
Launcher.ACTIVITY_TRACKER.registerCallback(
|
|
|
|
|
interactionListener,
|
2025-06-08 17:32:28 +00:00
|
|
|
HOME_SCREEN_WIDGET_INTERACTION_REASON_STRING,
|
2025-05-30 08:13:45 -07:00
|
|
|
)
|
|
|
|
|
startActivity(
|
|
|
|
|
/*intent=*/ Intent(Intent.ACTION_MAIN)
|
|
|
|
|
.addCategory(Intent.CATEGORY_HOME)
|
|
|
|
|
.setPackage(packageName)
|
|
|
|
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
2025-06-08 17:32:28 +00:00
|
|
|
/*options=*/ apiWrapper.createFadeOutAnimOptions().toBundle(),
|
2025-05-30 08:13:45 -07:00
|
|
|
)
|
|
|
|
|
finish()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
2025-06-08 17:32:28 +00:00
|
|
|
* pending drag [EXTRA_IS_PENDING_WIDGET_DRAG] (that would contain the widget info as part
|
|
|
|
|
* of clip data) that the host should be handling.
|
2025-05-30 08:13:45 -07:00
|
|
|
* - In case of add, finishes the activity with result containing extra information about
|
2025-06-08 17:32:28 +00:00
|
|
|
* the widget being added (namely [Intent.EXTRA_COMPONENT_NAME] and [Intent.EXTRA_USER].
|
2025-05-30 08:13:45 -07:00
|
|
|
*/
|
|
|
|
|
private fun WidgetPickerActivity.handleWidgetInteractionForExternalHost(
|
2025-06-08 17:32:28 +00:00
|
|
|
widgetInteractionInfo: WidgetInteractionInfo
|
2025-05-30 08:13:45 -07:00
|
|
|
) {
|
|
|
|
|
when (widgetInteractionInfo) {
|
|
|
|
|
is WidgetInteractionInfo.WidgetDragInfo ->
|
2025-06-08 17:32:28 +00:00
|
|
|
setResult(RESULT_OK, Intent().putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true))
|
2025-05-30 08:13:45 -07:00
|
|
|
|
|
|
|
|
is WidgetInteractionInfo.WidgetAddInfo -> {
|
2025-06-08 17:32:28 +00:00
|
|
|
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"
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-05-30 08:13:45 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
finish()
|
|
|
|
|
}
|
2025-06-02 22:34:00 -07:00
|
|
|
|
|
|
|
|
/** Builds the host constraints to provide to the widget picker module. */
|
2025-06-08 17:32:28 +00:00
|
|
|
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,
|
2025-06-02 22:34:00 -07:00
|
|
|
)
|
2025-06-08 17:32:28 +00:00
|
|
|
)
|
2025-06-02 22:34:00 -07:00
|
|
|
}
|
2025-06-08 17:32:28 +00:00
|
|
|
}
|
2025-05-30 08:13:45 -07:00
|
|
|
}
|
2025-05-29 13:26:00 -07:00
|
|
|
}
|