diff options
| author | 2024-12-13 10:26:45 -0800 | |
|---|---|---|
| committer | 2024-12-13 10:26:45 -0800 | |
| commit | 273179e2e3207c255d7f74e2275fcb1e04f6a425 (patch) | |
| tree | 232b4cff4e4005373bb8625e702701de1f0da633 | |
| parent | 1446e9594ac92eb7d6472ad1db6bbf4e72987eff (diff) | |
| parent | 8a5766b376f35a450a31f6e129aa0465009bcb85 (diff) | |
Merge "Simplify communal widget loading" into main
11 files changed, 379 insertions, 327 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index f36fd34d0445..5dbedc7045e4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -1398,6 +1398,7 @@ private fun WidgetContent( val shrinkWidgetLabel = stringResource(R.string.accessibility_action_label_shrink_widget) val expandWidgetLabel = stringResource(R.string.accessibility_action_label_expand_widget) + val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false) val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle() val selectedIndex = selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } } @@ -1511,7 +1512,8 @@ private fun WidgetContent( ) { with(widgetSection) { Widget( - viewModel = viewModel, + isFocusable = isFocusable, + openWidgetEditor = { viewModel.onOpenWidgetEditor() }, model = model, size = size, modifier = Modifier.fillMaxSize().allowGestures(allowed = !viewModel.isEditMode), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModelTest.kt new file mode 100644 index 000000000000..a8a3873d6de2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModelTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2024 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.systemui.communal.ui.viewmodel + +import android.appwidget.AppWidgetHost.AppWidgetHostListener +import android.appwidget.AppWidgetHostView +import android.platform.test.flag.junit.FlagsParameterization +import android.util.SizeF +import android.widget.RemoteViews +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_SECONDARY_USER_WIDGET_HOST +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper +import com.android.systemui.communal.widgets.AppWidgetHostListenerDelegate +import com.android.systemui.communal.widgets.CommunalAppWidgetHost +import com.android.systemui.communal.widgets.GlanceableHubWidgetManager +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundCoroutineContext +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.testKosmos +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +class CommunalAppWidgetViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { + val kosmos = testKosmos() + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + private val Kosmos.listenerDelegateFactory by + Kosmos.Fixture { + AppWidgetHostListenerDelegate.Factory { listener -> + AppWidgetHostListenerDelegate(fakeExecutor, listener) + } + } + + private val Kosmos.appWidgetHost by + Kosmos.Fixture { + mock<CommunalAppWidgetHost> { + on { setListener(any(), any()) } doAnswer + { invocation -> + val callback = invocation.arguments[1] as AppWidgetHostListener + callback.updateAppWidget(mock<RemoteViews>()) + } + } + } + + private val Kosmos.glanceableHubWidgetManager by + Kosmos.Fixture { + mock<GlanceableHubWidgetManager> { + on { setAppWidgetHostListener(any(), any()) } doAnswer + { invocation -> + val callback = invocation.arguments[1] as AppWidgetHostListener + callback.updateAppWidget(mock<RemoteViews>()) + } + } + } + + private val Kosmos.underTest by + Kosmos.Fixture { + CommunalAppWidgetViewModel( + backgroundCoroutineContext, + { appWidgetHost }, + listenerDelegateFactory, + { glanceableHubWidgetManager }, + fakeGlanceableHubMultiUserHelper, + ) + .apply { activateIn(testScope) } + } + + @Test + fun setListener() = + kosmos.runTest { + val listener = mock<AppWidgetHostListener>() + + underTest.setListener(123, listener) + runAll() + + verify(listener).updateAppWidget(any()) + } + + @Test + fun setListener_HSUM() = + kosmos.runTest { + fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true) + val listener = mock<AppWidgetHostListener>() + + underTest.setListener(123, listener) + runAll() + + verify(listener).updateAppWidget(any()) + } + + @Test + fun updateSize() = + kosmos.runTest { + val view = mock<AppWidgetHostView>() + val size = SizeF(/* width= */ 100f, /* height= */ 200f) + + underTest.updateSize(size, view) + runAll() + + verify(view) + .updateAppWidgetSize( + /* newOptions = */ any(), + /* minWidth = */ eq(100), + /* minHeight = */ eq(200), + /* maxWidth = */ eq(100), + /* maxHeight = */ eq(200), + /* ignorePadding = */ eq(true), + ) + } + + private fun Kosmos.runAll() { + runCurrent() + fakeExecutor.runAllReady() + } + + private companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(FLAG_SECONDARY_USER_WIDGET_HOST) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 9d711ab0cd29..d70af2806430 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -172,7 +172,6 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { kosmos.testDispatcher, testScope, kosmos.testScope.backgroundScope, - context.resources, kosmos.keyguardTransitionInteractor, kosmos.keyguardInteractor, mock<KeyguardIndicationController>(), diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 88ed4e353719..c54cfe5dbc86 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -280,7 +280,7 @@ <item type="id" name="udfps_accessibility_overlay_top_guideline" /> <!-- Ids for communal hub widgets --> - <item type="id" name="communal_widget_disposable_tag"/> + <item type="id" name="communal_widget_listener_tag"/> <!-- snapshot view-binding IDs --> <item type="id" name="snapshot_view_binding" /> diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalAppWidgetHostViewBinder.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalAppWidgetHostViewBinder.kt deleted file mode 100644 index 71bfe0c057e4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalAppWidgetHostViewBinder.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2024 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.systemui.communal.ui.binder - -import android.content.Context -import android.os.Bundle -import android.util.SizeF -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.compose.ui.unit.IntSize -import androidx.core.view.doOnLayout -import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.Flags.communalWidgetResizing -import com.android.systemui.common.ui.view.onLayoutChanged -import com.android.systemui.communal.domain.model.CommunalContentModel -import com.android.systemui.communal.util.WidgetViewFactory -import com.android.systemui.util.kotlin.DisposableHandles -import com.android.systemui.util.kotlin.toDp -import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn - -object CommunalAppWidgetHostViewBinder { - private const val TAG = "CommunalAppWidgetHostViewBinder" - - fun bind( - context: Context, - applicationScope: CoroutineScope, - mainContext: CoroutineContext, - backgroundContext: CoroutineContext, - container: FrameLayout, - model: CommunalContentModel.WidgetContent.Widget, - size: SizeF?, - factory: WidgetViewFactory, - ): DisposableHandle { - val disposables = DisposableHandles() - - val loadingJob = - applicationScope.launch("$TAG#createWidgetView") { - val widget = factory.createWidget(context, model, size) - waitForLayout(container) - container.post { container.setView(widget) } - if (communalWidgetResizing()) { - // Update the app widget size in the background. - launch("$TAG#updateSize", backgroundContext) { - container.sizeFlow().flowOn(mainContext).distinctUntilChanged().collect { - (width, height) -> - widget.updateAppWidgetSize( - /* newOptions = */ Bundle(), - /* minWidth = */ width, - /* minHeight = */ height, - /* maxWidth = */ width, - /* maxHeight = */ height, - /* ignorePadding = */ true, - ) - } - } - } - } - - disposables += DisposableHandle { loadingJob.cancel() } - disposables += DisposableHandle { container.removeAllViews() } - - return disposables - } - - private suspend fun waitForLayout(container: FrameLayout) = suspendCoroutine { cont -> - container.doOnLayout { cont.resume(Unit) } - } -} - -private fun ViewGroup.setView(view: View) { - if (view.parent == this) { - return - } - (view.parent as? ViewGroup)?.removeView(view) - addView(view) -} - -private fun View.sizeAsDp(): IntSize = IntSize(width.toDp(context), height.toDp(context)) - -private fun View.sizeFlow(): Flow<IntSize> = conflatedCallbackFlow { - if (isLaidOut && !isLayoutRequested) { - trySend(sizeAsDp()) - } - val disposable = onLayoutChanged { trySend(sizeAsDp()) } - awaitClose { disposable.dispose() } -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalAppWidgetSection.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalAppWidgetSection.kt index 2e12bad744f0..9f19562bb668 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalAppWidgetSection.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalAppWidgetSection.kt @@ -16,98 +16,132 @@ package com.android.systemui.communal.ui.view.layout.sections +import android.os.Bundle import android.util.SizeF +import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS -import android.widget.FrameLayout +import android.view.accessibility.AccessibilityNodeInfo import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.systemui.Flags.communalWidgetResizing +import com.android.systemui.Flags.communalHubUseThreadPoolForWidgets import com.android.systemui.communal.domain.model.CommunalContentModel -import com.android.systemui.communal.ui.binder.CommunalAppWidgetHostViewBinder -import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel -import com.android.systemui.communal.util.WidgetViewFactory -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.communal.ui.viewmodel.CommunalAppWidgetViewModel +import com.android.systemui.communal.widgets.CommunalAppWidgetHostView +import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.qualifiers.UiBackground +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R +import java.util.concurrent.Executor +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit import javax.inject.Inject -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DisposableHandle class CommunalAppWidgetSection @Inject constructor( - @Application private val applicationScope: CoroutineScope, - @Main private val mainContext: CoroutineContext, - @UiBackground private val backgroundContext: CoroutineContext, - private val factory: WidgetViewFactory, + @UiBackground private val uiBgExecutor: Executor, + private val interactionHandler: WidgetInteractionHandler, + private val viewModelFactory: CommunalAppWidgetViewModel.Factory, ) { private companion object { - val DISPOSABLE_TAG = R.id.communal_widget_disposable_tag + const val TAG = "CommunalAppWidgetSection" + val LISTENER_TAG = R.id.communal_widget_listener_tag + + val poolSize by lazy { Runtime.getRuntime().availableProcessors().coerceAtLeast(2) } + + /** + * This executor is used for widget inflation. Parameters match what launcher uses. See + * [com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR]. + */ + val widgetExecutor by lazy { + ThreadPoolExecutor( + /*corePoolSize*/ poolSize, + /*maxPoolSize*/ poolSize, + /*keepAlive*/ 1, + /*unit*/ TimeUnit.SECONDS, + /*workQueue*/ LinkedBlockingQueue(), + ) + } } @Composable fun Widget( - viewModel: BaseCommunalViewModel, + isFocusable: Boolean, + openWidgetEditor: () -> Unit, model: CommunalContentModel.WidgetContent.Widget, size: SizeF, modifier: Modifier = Modifier, ) { - val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false) + val viewModel = rememberViewModel("$TAG#viewModel") { viewModelFactory.create() } + val longClickLabel = stringResource(R.string.accessibility_action_label_edit_widgets) + val accessibilityDelegate = + remember(longClickLabel, openWidgetEditor) { + WidgetAccessibilityDelegate(longClickLabel, openWidgetEditor) + } AndroidView( factory = { context -> - FrameLayout(context).apply { - layoutParams = - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT, - ) - - // Need to attach the disposable handle to the view here instead of storing - // the state in the composable in order to properly support lazy lists. In a - // lazy list, when the composable is no longer in view - it will exit - // composition and any state inside the composable will be lost. However, - // the View instance will be re-used. Therefore we can store data on the view - // in order to preserve it. - setTag( - DISPOSABLE_TAG, - CommunalAppWidgetHostViewBinder.bind( - context = context, - container = this, - model = model, - size = if (!communalWidgetResizing()) size else null, - factory = factory, - applicationScope = applicationScope, - mainContext = mainContext, - backgroundContext = backgroundContext, - ), - ) - - accessibilityDelegate = viewModel.widgetAccessibilityDelegate + CommunalAppWidgetHostView(context, interactionHandler).apply { + if (communalHubUseThreadPoolForWidgets()) { + setExecutor(widgetExecutor) + } else { + setExecutor(uiBgExecutor) + } } }, - update = { container -> - container.importantForAccessibility = + update = { view -> + view.accessibilityDelegate = accessibilityDelegate + view.importantForAccessibility = if (isFocusable) { IMPORTANT_FOR_ACCESSIBILITY_AUTO } else { IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS } - }, - onRelease = { view -> - val disposable = (view.getTag(DISPOSABLE_TAG) as? DisposableHandle) - disposable?.dispose() + view.setAppWidget(model.appWidgetId, model.providerInfo) + // To avoid calling the expensive setListener method on every recomposition if + // the appWidgetId hasn't changed, we store the current appWidgetId of the view in + // a tag. + if ((view.getTag(LISTENER_TAG) as? Int) != model.appWidgetId) { + viewModel.setListener(model.appWidgetId, view) + view.setTag(LISTENER_TAG, model.appWidgetId) + } + viewModel.updateSize(size, view) }, modifier = modifier, // For reusing composition in lazy lists. onReset = {}, ) } + + private class WidgetAccessibilityDelegate( + private val longClickLabel: String, + private val longClickAction: () -> Unit, + ) : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) { + super.onInitializeAccessibilityNodeInfo(host, info) + // Hint user to long press in order to enter edit mode + info.addAction( + AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id, + longClickLabel.lowercase(), + ) + ) + } + + override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean { + when (action) { + AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id -> { + longClickAction() + return true + } + } + return super.performAccessibilityAction(host, action, args) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index a339af3694e7..099a85926020 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.communal.ui.viewmodel import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.os.UserHandle -import android.view.View import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey @@ -80,9 +79,6 @@ abstract class BaseCommunalViewModel( */ val glanceableTouchAvailable: Flow<Boolean> = anyOf(not(isTouchConsumed), isNestedScrolling) - /** Accessibility delegate to be set on CommunalAppWidgetHostView. */ - open val widgetAccessibilityDelegate: View.AccessibilityDelegate? = null - /** * The up-to-date value of the grid scroll offset. persisted to interactor on * {@link #persistScrollPosition} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModel.kt new file mode 100644 index 000000000000..6bafd14f9359 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModel.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2024 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.systemui.communal.ui.viewmodel + +import android.appwidget.AppWidgetHost.AppWidgetHostListener +import android.appwidget.AppWidgetHostView +import android.os.Bundle +import android.util.SizeF +import com.android.app.tracing.coroutines.coroutineScopeTraced +import com.android.app.tracing.coroutines.withContextTraced +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper +import com.android.systemui.communal.widgets.AppWidgetHostListenerDelegate +import com.android.systemui.communal.widgets.CommunalAppWidgetHost +import com.android.systemui.communal.widgets.GlanceableHubWidgetManager +import com.android.systemui.dagger.qualifiers.UiBackground +import com.android.systemui.lifecycle.ExclusiveActivatable +import dagger.Lazy +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow + +/** View model for showing a widget. */ +class CommunalAppWidgetViewModel +@AssistedInject +constructor( + @UiBackground private val backgroundContext: CoroutineContext, + private val appWidgetHostLazy: Lazy<CommunalAppWidgetHost>, + private val listenerDelegateFactory: AppWidgetHostListenerDelegate.Factory, + private val glanceableHubWidgetManagerLazy: Lazy<GlanceableHubWidgetManager>, + private val multiUserHelper: GlanceableHubMultiUserHelper, +) : ExclusiveActivatable() { + + private companion object { + const val TAG = "CommunalAppWidgetViewModel" + const val CHANNEL_CAPACITY = 10 + } + + @AssistedFactory + interface Factory { + fun create(): CommunalAppWidgetViewModel + } + + private val requests = + Channel<Request>(capacity = CHANNEL_CAPACITY, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + fun setListener(appWidgetId: Int, listener: AppWidgetHostListener) { + requests.trySend(SetListener(appWidgetId, listener)) + } + + fun updateSize(size: SizeF, view: AppWidgetHostView) { + requests.trySend(UpdateSize(size, view)) + } + + override suspend fun onActivated(): Nothing { + coroutineScopeTraced("$TAG#onActivated") { + requests.receiveAsFlow().collect { request -> + when (request) { + is SetListener -> handleSetListener(request.appWidgetId, request.listener) + is UpdateSize -> handleUpdateSize(request.size, request.view) + } + } + } + + awaitCancellation() + } + + private suspend fun handleSetListener(appWidgetId: Int, listener: AppWidgetHostListener) = + withContextTraced("$TAG#setListenerInner", backgroundContext) { + if ( + multiUserHelper.glanceableHubHsumFlagEnabled && + multiUserHelper.isInHeadlessSystemUser() + ) { + // If the widget view is created in the headless system user, the widget host lives + // remotely in the foreground user, and therefore the host listener needs to be + // registered through the widget manager. + with(glanceableHubWidgetManagerLazy.get()) { + setAppWidgetHostListener(appWidgetId, listenerDelegateFactory.create(listener)) + } + } else { + // Instead of setting the view as the listener directly, we wrap the view in a + // delegate which ensures the callbacks always get called on the main thread. + with(appWidgetHostLazy.get()) { + setListener(appWidgetId, listenerDelegateFactory.create(listener)) + } + } + } + + private suspend fun handleUpdateSize(size: SizeF, view: AppWidgetHostView) = + withContextTraced("$TAG#updateSizeInner", backgroundContext) { + view.updateAppWidgetSize( + /* newOptions = */ Bundle(), + /* minWidth = */ size.width.toInt(), + /* minHeight = */ size.height.toInt(), + /* maxWidth = */ size.width.toInt(), + /* maxHeight = */ size.height.toInt(), + /* ignorePadding = */ true, + ) + } +} + +private sealed interface Request + +/** + * [Request] to call [CommunalAppWidgetHost.setListener] to tie this view to a particular widget. + * Since this is involves an IPC to system_server, the call is asynchronous and happens in the + * background. + */ +private data class SetListener(val appWidgetId: Int, val listener: AppWidgetHostListener) : Request + +/** + * [Request] to call [AppWidgetHostView.updateAppWidgetSize] to notify the widget provider of the + * new size. Since this is involves an IPC to system_server, the call is asynchronous and happens in + * the background. + */ +private data class UpdateSize(val size: SizeF, val view: AppWidgetHostView) : Request diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 83bd265db5f6..ddc4d1c10690 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -17,10 +17,6 @@ package com.android.systemui.communal.ui.viewmodel import android.content.ComponentName -import android.content.res.Resources -import android.os.Bundle -import android.view.View -import android.view.accessibility.AccessibilityNodeInfo import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags import com.android.systemui.communal.domain.interactor.CommunalInteractor @@ -45,7 +41,6 @@ import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.dagger.MediaModule -import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.KeyguardIndicationController @@ -85,7 +80,6 @@ constructor( @Main val mainDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, @Background private val bgScope: CoroutineScope, - @Main private val resources: Resources, keyguardTransitionInteractor: KeyguardTransitionInteractor, keyguardInteractor: KeyguardInteractor, private val keyguardIndicationController: KeyguardIndicationController, @@ -219,39 +213,6 @@ constructor( } .distinctUntilChanged() - override val widgetAccessibilityDelegate = - object : View.AccessibilityDelegate() { - override fun onInitializeAccessibilityNodeInfo( - host: View, - info: AccessibilityNodeInfo, - ) { - super.onInitializeAccessibilityNodeInfo(host, info) - // Hint user to long press in order to enter edit mode - info.addAction( - AccessibilityNodeInfo.AccessibilityAction( - AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id, - resources - .getString(R.string.accessibility_action_label_edit_widgets) - .lowercase(), - ) - ) - } - - override fun performAccessibilityAction( - host: View, - action: Int, - args: Bundle?, - ): Boolean { - when (action) { - AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id -> { - onOpenWidgetEditor() - return true - } - } - return super.performAccessibilityAction(host, action, args) - } - } - private val _isEnableWidgetDialogShowing: MutableStateFlow<Boolean> = MutableStateFlow(false) val isEnableWidgetDialogShowing: Flow<Boolean> = _isEnableWidgetDialogShowing.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt b/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt deleted file mode 100644 index 50d86a24be96..000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2024 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.systemui.communal.util - -import android.content.Context -import android.os.Bundle -import android.util.SizeF -import com.android.app.tracing.coroutines.withContextTraced as withContext -import com.android.systemui.Flags -import com.android.systemui.communal.domain.model.CommunalContentModel -import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper -import com.android.systemui.communal.widgets.AppWidgetHostListenerDelegate -import com.android.systemui.communal.widgets.CommunalAppWidgetHost -import com.android.systemui.communal.widgets.CommunalAppWidgetHostView -import com.android.systemui.communal.widgets.GlanceableHubWidgetManager -import com.android.systemui.communal.widgets.WidgetInteractionHandler -import com.android.systemui.dagger.qualifiers.UiBackground -import dagger.Lazy -import java.util.concurrent.Executor -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.ThreadPoolExecutor -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import kotlin.coroutines.CoroutineContext - -/** Factory for creating [CommunalAppWidgetHostView] in a background thread. */ -class WidgetViewFactory -@Inject -constructor( - @UiBackground private val uiBgContext: CoroutineContext, - @UiBackground private val uiBgExecutor: Executor, - private val appWidgetHostLazy: Lazy<CommunalAppWidgetHost>, - private val interactionHandler: WidgetInteractionHandler, - private val listenerFactory: AppWidgetHostListenerDelegate.Factory, - private val glanceableHubWidgetManagerLazy: Lazy<GlanceableHubWidgetManager>, - private val multiUserHelper: GlanceableHubMultiUserHelper, -) { - suspend fun createWidget( - context: Context, - model: CommunalContentModel.WidgetContent.Widget, - size: SizeF?, - ): CommunalAppWidgetHostView = - withContext("$TAG#createWidget", uiBgContext) { - val view = - CommunalAppWidgetHostView(context, interactionHandler).apply { - if (Flags.communalHubUseThreadPoolForWidgets()) { - setExecutor(widgetExecutor) - } else { - setExecutor(uiBgExecutor) - } - setAppWidget(model.appWidgetId, model.providerInfo) - } - - if ( - multiUserHelper.glanceableHubHsumFlagEnabled && - multiUserHelper.isInHeadlessSystemUser() - ) { - // If the widget view is created in the headless system user, the widget host lives - // remotely in the foreground user, and therefore the host listener needs to be - // registered through the widget manager. - with(glanceableHubWidgetManagerLazy.get()) { - setAppWidgetHostListener(model.appWidgetId, listenerFactory.create(view)) - } - } else { - // Instead of setting the view as the listener directly, we wrap the view in a - // delegate which ensures the callbacks always get called on the main thread. - with(appWidgetHostLazy.get()) { - setListener(model.appWidgetId, listenerFactory.create(view)) - } - } - - if (size != null) { - view.updateAppWidgetSize( - /* newOptions = */ Bundle(), - /* minWidth = */ size.width.toInt(), - /* minHeight = */ size.height.toInt(), - /* maxWidth = */ size.width.toInt(), - /* maxHeight = */ size.height.toInt(), - /* ignorePadding = */ true, - ) - } - view - } - - private companion object { - const val TAG = "WidgetViewFactory" - - val poolSize = Runtime.getRuntime().availableProcessors().coerceAtLeast(2) - - /** - * This executor is used for widget inflation. Parameters match what launcher uses. See - * [com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR]. - */ - val widgetExecutor = - ThreadPoolExecutor( - /*corePoolSize*/ poolSize, - /*maxPoolSize*/ poolSize, - /*keepAlive*/ 1, - /*unit*/ TimeUnit.SECONDS, - /*workQueue*/ LinkedBlockingQueue(), - ) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/AppWidgetHostListenerDelegate.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/AppWidgetHostListenerDelegate.kt index f3416216afdd..7d80acd1f439 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/AppWidgetHostListenerDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/AppWidgetHostListenerDelegate.kt @@ -36,7 +36,7 @@ constructor( ) : AppWidgetHostListener { @AssistedFactory - interface Factory { + fun interface Factory { fun create(listener: AppWidgetHostListener): AppWidgetHostListenerDelegate } |