diff options
| author | 2024-08-12 21:05:20 +0000 | |
|---|---|---|
| committer | 2024-08-12 21:05:20 +0000 | |
| commit | 29b176054dba6f38c53c32b28d366187239a269f (patch) | |
| tree | 7503881035d9ad9379791db8150b924993997231 | |
| parent | 216cea0f5686d9b7d8fb79bc310d758461f6421d (diff) | |
| parent | 571167562075c8853f89bac9b7e851680099b8bb (diff) | |
Merge "Create widget host views in background thread" into main
8 files changed, 272 insertions, 39 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt index 8c38253d6469..6fca1785d768 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt @@ -28,6 +28,7 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.communal.smartspace.SmartspaceInteractionHandler import com.android.systemui.communal.ui.compose.section.AmbientStatusBarSection import com.android.systemui.communal.ui.compose.section.CommunalPopupSection +import com.android.systemui.communal.ui.view.layout.sections.CommunalAppWidgetSection import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection @@ -46,6 +47,7 @@ constructor( private val bottomAreaSection: BottomAreaSection, private val ambientStatusBarSection: AmbientStatusBarSection, private val communalPopupSection: CommunalPopupSection, + private val widgetSection: CommunalAppWidgetSection, ) { @Composable @@ -63,6 +65,7 @@ constructor( viewModel = viewModel, interactionHandler = interactionHandler, dialogFactory = dialogFactory, + widgetSection = widgetSection, modifier = Modifier.element(Communal.Elements.Grid) ) } 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 b65b47123eaa..b93b0490816d 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 @@ -19,10 +19,7 @@ package com.android.systemui.communal.ui.compose import android.content.Context import android.content.res.Configuration import android.graphics.drawable.Icon -import android.os.Bundle import android.util.SizeF -import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO -import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS import android.widget.FrameLayout import android.widget.RemoteViews import androidx.annotation.VisibleForTesting @@ -105,7 +102,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.CornerRadius @@ -169,6 +165,7 @@ import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset import com.android.systemui.communal.ui.compose.extensions.observeTaps +import com.android.systemui.communal.ui.view.layout.sections.CommunalAppWidgetSection import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel @@ -180,11 +177,12 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialogFactory import kotlinx.coroutines.launch -@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun CommunalHub( modifier: Modifier = Modifier, viewModel: BaseCommunalViewModel, + widgetSection: CommunalAppWidgetSection, interactionHandler: RemoteViews.InteractionHandler? = null, dialogFactory: SystemUIDialogFactory? = null, widgetConfigurator: WidgetConfigurator? = null, @@ -383,6 +381,7 @@ fun CommunalHub( selectedKey = selectedKey, widgetConfigurator = widgetConfigurator, interactionHandler = interactionHandler, + widgetSection = widgetSection, ) } } @@ -632,6 +631,7 @@ private fun BoxScope.CommunalHubLazyGrid( updateDragPositionForRemove: (offset: Offset) -> Boolean, widgetConfigurator: WidgetConfigurator?, interactionHandler: RemoteViews.InteractionHandler?, + widgetSection: CommunalAppWidgetSection, ) { var gridModifier = Modifier.align(Alignment.TopStart).onGloballyPositioned { setGridCoordinates(it) } @@ -719,18 +719,20 @@ private fun BoxScope.CommunalHubLazyGrid( index = index, contentListState = contentListState, interactionHandler = interactionHandler, + widgetSection = widgetSection, ) } } else { CommunalContent( - modifier = cardModifier.animateItem(), model = list[index], viewModel = viewModel, size = size, selected = false, + modifier = cardModifier.animateItem(), index = index, contentListState = contentListState, interactionHandler = interactionHandler, + widgetSection = widgetSection, ) } } @@ -972,6 +974,7 @@ private fun CommunalContent( index: Int, contentListState: ContentListState, interactionHandler: RemoteViews.InteractionHandler?, + widgetSection: CommunalAppWidgetSection, ) { when (model) { is CommunalContentModel.WidgetContent.Widget -> @@ -983,7 +986,8 @@ private fun CommunalContent( widgetConfigurator, modifier, index, - contentListState + contentListState, + widgetSection, ) is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier) is CommunalContentModel.WidgetContent.DisabledWidget -> @@ -1116,9 +1120,9 @@ private fun WidgetContent( modifier: Modifier = Modifier, index: Int, contentListState: ContentListState, + widgetSection: CommunalAppWidgetSection, ) { val context = LocalContext.current - val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false) val accessibilityLabel = remember(model, context) { model.providerInfo.loadLabel(context.packageManager).toString().trim() @@ -1205,36 +1209,14 @@ private fun WidgetContent( } } ) { - AndroidView( - modifier = Modifier.fillMaxSize().allowGestures(allowed = !viewModel.isEditMode), - factory = { context -> - model.appWidgetHost - .createViewForCommunal(context, model.appWidgetId, model.providerInfo) - .apply { - updateAppWidgetSize( - /* newOptions = */ Bundle(), - /* minWidth = */ size.width.toInt(), - /* minHeight = */ size.height.toInt(), - /* maxWidth = */ size.width.toInt(), - /* maxHeight = */ size.height.toInt(), - /* ignorePadding = */ true - ) - accessibilityDelegate = viewModel.widgetAccessibilityDelegate - } - }, - update = { - it.apply { - importantForAccessibility = - if (isFocusable) { - IMPORTANT_FOR_ACCESSIBILITY_AUTO - } else { - IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - } - } - }, - // For reusing composition in lazy lists. - onReset = {}, - ) + with(widgetSection) { + Widget( + viewModel = viewModel, + model = model, + size = size, + modifier = Modifier.fillMaxSize().allowGestures(allowed = !viewModel.isEditMode), + ) + } if ( viewModel is CommunalEditModeViewModel && model.reconfigurable && diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt index 5886d7de47b9..6750e41009c7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt @@ -23,6 +23,7 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.communal.ui.view.layout.sections.CommunalAppWidgetSection import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.SysUISingleton @@ -42,6 +43,7 @@ constructor( private val viewModel: CommunalViewModel, private val dialogFactory: SystemUIDialogFactory, private val interactionHandler: WidgetInteractionHandler, + private val widgetSection: CommunalAppWidgetSection, ) : ComposableScene { override val key = Scenes.Communal @@ -55,6 +57,12 @@ constructor( @Composable override fun SceneScope.Content(modifier: Modifier) { - CommunalHub(modifier, viewModel, interactionHandler, dialogFactory) + CommunalHub( + modifier = modifier, + viewModel = viewModel, + interactionHandler = interactionHandler, + widgetSection = widgetSection, + dialogFactory = dialogFactory, + ) } } diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index e4f900d3a31a..25bca25be732 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -278,4 +278,7 @@ <!-- Id for the udfps accessibility overlay --> <item type="id" name="udfps_accessibility_overlay" /> <item type="id" name="udfps_accessibility_overlay_top_guideline" /> + + <!-- Ids for communal hub widgets --> + <item type="id" name="communal_widget_disposable_tag"/> </resources> 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 new file mode 100644 index 000000000000..7a0567190bd0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalAppWidgetHostViewBinder.kt @@ -0,0 +1,69 @@ +/* + * 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.util.SizeF +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import com.android.app.tracing.coroutines.launch +import com.android.systemui.communal.domain.model.CommunalContentModel +import com.android.systemui.communal.util.WidgetViewFactory +import com.android.systemui.util.kotlin.DisposableHandles +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle + +object CommunalAppWidgetHostViewBinder { + private const val TAG = "CommunalAppWidgetHostViewBinder" + + fun bind( + context: Context, + applicationScope: CoroutineScope, + 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) + // TODO(b/358662507): Remove this workaround + (container.parent as? ViewGroup)?.let { parent -> + val index = parent.indexOfChild(container) + parent.removeView(container) + parent.addView(container, index) + } + container.setView(widget) + } + + disposables += DisposableHandle { loadingJob.cancel() } + disposables += DisposableHandle { container.removeAllViews() } + + return disposables + } +} + +private fun ViewGroup.setView(view: View) { + if (view.parent == this) { + return + } + (view.parent as? ViewGroup)?.removeView(view) + addView(view) +} 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 new file mode 100644 index 000000000000..56b769e7bc13 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalAppWidgetSection.kt @@ -0,0 +1,105 @@ +/* + * 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.view.layout.sections + +import android.util.SizeF +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS +import android.widget.FrameLayout +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.compose.collectAsStateWithLifecycle +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.res.R +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle + +class CommunalAppWidgetSection +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val factory: WidgetViewFactory, +) { + + private companion object { + val DISPOSABLE_TAG = R.id.communal_widget_disposable_tag + } + + @Composable + fun Widget( + viewModel: BaseCommunalViewModel, + model: CommunalContentModel.WidgetContent.Widget, + size: SizeF, + modifier: Modifier = Modifier, + ) { + val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false) + + 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 = size, + factory = factory, + applicationScope = applicationScope, + ) + ) + + accessibilityDelegate = viewModel.widgetAccessibilityDelegate + } + }, + update = { container -> + container.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() + }, + modifier = modifier, + // For reusing composition in lazy lists. + onReset = {}, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt b/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt new file mode 100644 index 000000000000..0e39a99765bd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt @@ -0,0 +1,60 @@ +/* + * 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.withContext +import com.android.systemui.communal.domain.model.CommunalContentModel +import com.android.systemui.communal.widgets.CommunalAppWidgetHost +import com.android.systemui.communal.widgets.CommunalAppWidgetHostView +import com.android.systemui.dagger.qualifiers.UiBackground +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, + private val appWidgetHost: CommunalAppWidgetHost, +) { + suspend fun createWidget( + context: Context, + model: CommunalContentModel.WidgetContent.Widget, + size: SizeF, + ): CommunalAppWidgetHostView = + withContext("$TAG#createWidget", uiBgContext) { + appWidgetHost + .createViewForCommunal(context, model.appWidgetId, model.providerInfo) + .apply { + updateAppWidgetSize( + /* newOptions = */ Bundle(), + /* minWidth = */ size.width.toInt(), + /* minHeight = */ size.height.toInt(), + /* maxWidth = */ size.width.toInt(), + /* maxHeight = */ size.height.toInt(), + /* ignorePadding = */ true, + ) + } + } + + private companion object { + const val TAG = "WidgetViewFactory" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 2bcbc9aa74ac..668fef6130bb 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -42,6 +42,7 @@ import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys import com.android.systemui.communal.shared.model.EditModeState import com.android.systemui.communal.ui.compose.CommunalHub +import com.android.systemui.communal.ui.view.layout.sections.CommunalAppWidgetSection import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtraFromIntent import com.android.systemui.keyguard.shared.model.KeyguardState @@ -60,6 +61,7 @@ constructor( private var windowManagerService: IWindowManager? = null, private val uiEventLogger: UiEventLogger, private val widgetConfiguratorFactory: WidgetConfigurationController.Factory, + private val widgetSection: CommunalAppWidgetSection, @CommunalLog logBuffer: LogBuffer, ) : ComponentActivity() { companion object { @@ -204,6 +206,7 @@ constructor( onOpenWidgetPicker = ::onOpenWidgetPicker, widgetConfigurator = widgetConfigurator, onEditDone = ::onEditDone, + widgetSection = widgetSection, ) } } |