Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b2eedb98 authored by Darrell Shi's avatar Darrell Shi Committed by William Xiao
Browse files

Move widget view creation from ViewModel to Compose

The current flow of the communal hub creates an AppWidgetHostView each
time the widget flow is updated. This view creation logic should live
inside the AndroidView Composable, and only called once on factory.

This change also replaces the CommunalContentUiModel with
CommunalContentModel, which lives in the domain layer.

Test: atest CommunalInteractorTest
Bug: 308148193
Flag: ACONFIG com.android.systemui.communal_hub DEVELOPMENT

Change-Id: I3216a70c82973d75de79247605985a9cc0262c8b
parent 0fc48a2a
Loading
Loading
Loading
Loading
+45 −69
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.systemui.communal.ui.compose

import android.appwidget.AppWidgetHostView
import android.os.Bundle
import android.util.SizeF
import androidx.compose.foundation.background
@@ -24,7 +23,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
@@ -44,8 +42,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.ui.model.CommunalContentUiModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.res.R

@@ -54,8 +52,7 @@ fun CommunalHub(
    modifier: Modifier = Modifier,
    viewModel: CommunalViewModel,
) {
    val showTutorial by viewModel.showTutorialContent.collectAsState(initial = false)
    val widgetContent by viewModel.widgetContent.collectAsState(initial = emptyList())
    val communalContent by viewModel.communalContent.collectAsState(initial = emptyList())
    Box(
        modifier = modifier.fillMaxSize().background(Color.White),
    ) {
@@ -65,32 +62,21 @@ fun CommunalHub(
            horizontalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
            verticalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
        ) {
            if (showTutorial) {
            items(
                    count = tutorialContentSizes.size,
                    // TODO(b/308148193): a more scalable solution for unique ids.
                    key = { index -> "tutorial_$index" },
                    span = { index -> GridItemSpan(tutorialContentSizes[index].span) },
                count = communalContent.size,
                key = { index -> communalContent[index].key },
                span = { index -> GridItemSpan(communalContent[index].size.span) },
            ) { index ->
                    TutorialCard(
                        modifier =
                            Modifier.size(Dimensions.CardWidth, tutorialContentSizes[index].dp()),
                CommunalContent(
                    model = communalContent[index],
                    deleteOnClick = viewModel::onDeleteWidget,
                    size =
                        SizeF(
                            Dimensions.CardWidth.value,
                            communalContent[index].size.dp().value,
                        ),
                )
            }
            } else {
                items(
                    count = widgetContent.size,
                    key = { index -> widgetContent[index].id },
                    span = { index -> GridItemSpan(widgetContent[index].size.span) },
                ) { index ->
                    val widget = widgetContent[index]
                    ContentCard(
                        modifier = Modifier.size(Dimensions.CardWidth, widget.size.dp()),
                        model = widget,
                        deleteOnClick = viewModel::onDeleteWidget
                    )
                }
            }
        }
        IconButton(onClick = viewModel::onOpenWidgetPicker) {
            Icon(
@@ -101,15 +87,23 @@ fun CommunalHub(
    }
}

// A placeholder for tutorial content.
@Composable
private fun TutorialCard(modifier: Modifier = Modifier) {
    Card(modifier = modifier, content = {})
private fun CommunalContent(
    model: CommunalContentModel,
    size: SizeF,
    deleteOnClick: (id: Int) -> Unit,
    modifier: Modifier = Modifier,
) {
    when (model) {
        is CommunalContentModel.Widget -> WidgetContent(model, size, deleteOnClick, modifier)
        is CommunalContentModel.Tutorial -> TutorialContent(modifier)
    }
}

@Composable
private fun ContentCard(
    model: CommunalContentUiModel,
private fun WidgetContent(
    model: CommunalContentModel.Widget,
    size: SizeF,
    deleteOnClick: (id: Int) -> Unit,
    modifier: Modifier = Modifier,
) {
@@ -117,31 +111,30 @@ private fun ContentCard(
    Box(
        modifier = modifier.fillMaxSize().background(Color.White),
    ) {
        // TODO(b/308148193): this will be cleaned up soon once the change to convert to
        // CommunalContentUiModel interface is merged
        val widgetId = getWidgetId(model.id)
        widgetId?.let {
            IconButton(onClick = { deleteOnClick(it) }) {
        IconButton(onClick = { deleteOnClick(model.appWidgetId) }) {
            Icon(
                Icons.Default.Close,
                LocalContext.current.getString(R.string.button_to_remove_widget)
            )
        }
        }
        AndroidView(
            modifier = modifier,
            factory = {
                model.view.apply {
                    if (this is AppWidgetHostView) {
                        val size = SizeF(Dimensions.CardWidth.value, model.size.dp().value)
                        updateAppWidgetSize(Bundle.EMPTY, listOf(size))
                    }
                }
            factory = { context ->
                model.appWidgetHost
                    .createView(context, model.appWidgetId, model.providerInfo)
                    .apply { updateAppWidgetSize(Bundle.EMPTY, listOf(size)) }
            },
            // For reusing composition in lazy lists.
            onReset = {}
        )
    }
}

@Composable
private fun TutorialContent(modifier: Modifier = Modifier) {
    Card(modifier = modifier, content = {})
}

private fun CommunalContentSize.dp(): Dp {
    return when (this) {
        CommunalContentSize.FULL -> Dimensions.CardHeightFull
@@ -150,23 +143,6 @@ private fun CommunalContentSize.dp(): Dp {
    }
}

private fun getWidgetId(id: String): Int? {
    return if (id.startsWith("widget_")) id.substring("widget_".length).toInt() else null
}

// Sizes for the tutorial placeholders.
private val tutorialContentSizes =
    listOf(
        CommunalContentSize.FULL,
        CommunalContentSize.THIRD,
        CommunalContentSize.THIRD,
        CommunalContentSize.THIRD,
        CommunalContentSize.HALF,
        CommunalContentSize.HALF,
        CommunalContentSize.HALF,
        CommunalContentSize.HALF,
    )

private object Dimensions {
    val CardWidth = 464.dp
    val CardHeightFull = 630.dp
+43 −9
Original line number Diff line number Diff line
@@ -16,16 +16,21 @@

package com.android.systemui.communal.domain.interactor

import android.appwidget.AppWidgetHost
import android.content.ComponentName
import com.android.systemui.communal.data.repository.CommunalRepository
import com.android.systemui.communal.data.repository.CommunalWidgetRepository
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map

/** Encapsulates business-logic related to communal mode. */
@@ -35,6 +40,8 @@ class CommunalInteractor
constructor(
    private val communalRepository: CommunalRepository,
    private val widgetRepository: CommunalWidgetRepository,
    tutorialInteractor: CommunalTutorialInteractor,
    private val appWidgetHost: AppWidgetHost,
) {

    /** Whether communal features are enabled. */
@@ -44,14 +51,6 @@ constructor(
    /** A flow of info about the widget to be displayed, or null if widget is unavailable. */
    val appWidgetInfo: Flow<CommunalAppWidgetInfo?> = widgetRepository.stopwatchAppWidgetInfo

    /**
     * A flow of information about widgets to be shown in communal hub.
     *
     * Currently only showing persistent widgets that have been bound to the app widget service
     * (have an allocated id).
     */
    val widgetContent: Flow<List<CommunalWidgetContentModel>> = widgetRepository.communalWidgets

    /**
     * Target scene as requested by the underlying [SceneTransitionLayout] or through
     * [onSceneChanged].
@@ -76,4 +75,39 @@ constructor(

    /** Delete a widget by id. */
    fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id)

    /** A list of all the communal content to be displayed in the communal hub. */
    @OptIn(ExperimentalCoroutinesApi::class)
    val communalContent: Flow<List<CommunalContentModel>> =
        tutorialInteractor.isTutorialAvailable.flatMapLatest { isTutorialMode ->
            if (isTutorialMode) {
                return@flatMapLatest flowOf(tutorialContent)
            }
            widgetContent
        }

    /** A list of widget content to be displayed in the communal hub. */
    private val widgetContent: Flow<List<CommunalContentModel.Widget>> =
        widgetRepository.communalWidgets.map { widgets ->
            widgets.map Widget@{ widget ->
                return@Widget CommunalContentModel.Widget(
                    appWidgetId = widget.appWidgetId,
                    providerInfo = widget.providerInfo,
                    appWidgetHost = appWidgetHost,
                )
            }
        }

    /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */
    private val tutorialContent: List<CommunalContentModel.Tutorial> =
        listOf(
            CommunalContentModel.Tutorial(id = 0, CommunalContentSize.FULL),
            CommunalContentModel.Tutorial(id = 1, CommunalContentSize.THIRD),
            CommunalContentModel.Tutorial(id = 2, CommunalContentSize.THIRD),
            CommunalContentModel.Tutorial(id = 3, CommunalContentSize.THIRD),
            CommunalContentModel.Tutorial(id = 4, CommunalContentSize.HALF),
            CommunalContentModel.Tutorial(id = 5, CommunalContentSize.HALF),
            CommunalContentModel.Tutorial(id = 6, CommunalContentSize.HALF),
            CommunalContentModel.Tutorial(id = 7, CommunalContentSize.HALF),
        )
}
+47 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.domain.model

import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetProviderInfo
import com.android.systemui.communal.shared.model.CommunalContentSize

/** Encapsulates data for a communal content. */
sealed interface CommunalContentModel {
    /** Unique key across all types of content models. */
    val key: String

    /** Size to be rendered in the grid. */
    val size: CommunalContentSize

    class Widget(
        val appWidgetId: Int,
        val providerInfo: AppWidgetProviderInfo,
        val appWidgetHost: AppWidgetHost,
    ) : CommunalContentModel {
        override val key = "widget_$appWidgetId"
        // Widget size is always half.
        override val size = CommunalContentSize.HALF
    }

    class Tutorial(
        id: Int,
        override val size: CommunalContentSize,
    ) : CommunalContentModel {
        override val key = "tutorial_$id"
    }
}
+0 −15
Original line number Diff line number Diff line
package com.android.systemui.communal.ui.model

import android.view.View
import com.android.systemui.communal.shared.model.CommunalContentSize

/**
 * Encapsulates data for a communal content that holds a view.
 *
 * This model stays in the UI layer.
 */
data class CommunalContentUiModel(
    val id: String,
    val view: View,
    val size: CommunalContentSize = CommunalContentSize.HALF,
)
+4 −28
Original line number Diff line number Diff line
@@ -16,53 +16,29 @@

package com.android.systemui.communal.ui.viewmodel

import android.appwidget.AppWidgetHost
import android.content.ComponentName
import android.content.Context
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.ui.model.CommunalContentUiModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map

@SysUISingleton
class CommunalViewModel
@Inject
constructor(
    @Application private val context: Context,
    private val appWidgetHost: AppWidgetHost,
    private val communalInteractor: CommunalInteractor,
    tutorialInteractor: CommunalTutorialInteractor,
) {
    /** Whether communal hub should show tutorial content. */
    val showTutorialContent: Flow<Boolean> = tutorialInteractor.isTutorialAvailable

    /** List of widgets to be displayed in the communal hub. */
    val widgetContent: Flow<List<CommunalContentUiModel>> =
        communalInteractor.widgetContent.map { widgets ->
            widgets.map Widget@{ widget ->
                // TODO(b/306406256): As adding and removing widgets functionalities are
                // supported, cache the host views so they're not recreated each time.
                val hostView =
                    appWidgetHost.createView(context, widget.appWidgetId, widget.providerInfo)
                return@Widget CommunalContentUiModel(
                    // TODO(b/308148193): a more scalable solution for unique ids.
                    id = "widget_${widget.appWidgetId}",
                    view = hostView,
                )
            }
        }

    val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene
    fun onSceneChanged(scene: CommunalSceneKey) {
        communalInteractor.onSceneChanged(scene)
    }

    /** A list of all the communal content to be displayed in the communal hub. */
    val communalContent: Flow<List<CommunalContentModel>> = communalInteractor.communalContent

    /** Delete a widget by id. */
    fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)

Loading