summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Lucas Silva <lusilva@google.com> 2024-12-13 10:26:45 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2024-12-13 10:26:45 -0800
commit273179e2e3207c255d7f74e2275fcb1e04f6a425 (patch)
tree232b4cff4e4005373bb8625e702701de1f0da633
parent1446e9594ac92eb7d6472ad1db6bbf4e72987eff (diff)
parent8a5766b376f35a450a31f6e129aa0465009bcb85 (diff)
Merge "Simplify communal widget loading" into main
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModelTest.kt154
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt1
-rw-r--r--packages/SystemUI/res/values/ids.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalAppWidgetHostViewBinder.kt110
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalAppWidgetSection.kt140
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModel.kt133
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt117
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/AppWidgetHostListenerDelegate.kt2
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
}