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

Commit 47d2bba6 authored by Darrell Shi's avatar Darrell Shi Committed by Android (Google) Code Review
Browse files

Merge changes I17be1804,I3216a70c,I383b4d15,Iaddf56eb into main

* changes:
  Smartspace in communal hub
  Move widget view creation from ViewModel to Compose
  Remove dependency on CommunalInteractor
  Move CommunalTutorialInteractorTest to multivalentTests
parents c2d8bf26 c094ec2f
Loading
Loading
Loading
Loading
+65 −69
Original line number Diff line number Diff line
@@ -16,15 +16,16 @@

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

import android.appwidget.AppWidgetHostView
import android.os.Bundle
import android.util.SizeF
import android.widget.FrameLayout
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
@@ -44,8 +45,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 +55,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,33 +65,23 @@ 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()),
                    )
                }
            } 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
                CommunalContent(
                    modifier = Modifier.fillMaxHeight().width(Dimensions.CardWidth),
                    model = communalContent[index],
                    deleteOnClick = viewModel::onDeleteWidget,
                    size =
                        SizeF(
                            Dimensions.CardWidth.value,
                            communalContent[index].size.dp().value,
                        ),
                )
            }
        }
        }
        IconButton(onClick = viewModel::onOpenWidgetPicker) {
            Icon(
                Icons.Default.Add,
@@ -101,15 +91,24 @@ 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.Smartspace -> SmartspaceContent(model, 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,29 +116,43 @@ 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 SmartspaceContent(
    model: CommunalContentModel.Smartspace,
    modifier: Modifier = Modifier,
) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            FrameLayout(context).apply { addView(model.remoteViews.apply(context, this)) }
        },
        // For reusing composition in lazy lists.
        onReset = {}
    )
}

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

private fun CommunalContentSize.dp(): Dp {
@@ -150,23 +163,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
+119 −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.data.repository

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalRepositoryImplTest : SysuiTestCase() {
    private lateinit var underTest: CommunalRepositoryImpl

    private lateinit var testScope: TestScope

    private lateinit var featureFlagsClassic: FakeFeatureFlagsClassic
    private lateinit var sceneContainerFlags: FakeSceneContainerFlags
    private lateinit var sceneContainerRepository: SceneContainerRepository

    @Before
    fun setUp() {
        testScope = TestScope()

        val sceneTestUtils = SceneTestUtils(this)
        sceneContainerFlags = FakeSceneContainerFlags(enabled = false)
        sceneContainerRepository = sceneTestUtils.fakeSceneContainerRepository()
        featureFlagsClassic = FakeFeatureFlagsClassic()

        featureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)

        underTest =
            CommunalRepositoryImpl(
                featureFlagsClassic,
                sceneContainerFlags,
                sceneContainerRepository,
            )
    }

    @Test
    fun isCommunalShowing_sceneContainerDisabled_onCommunalScene_true() =
        testScope.runTest {
            underTest.setDesiredScene(CommunalSceneKey.Communal)

            val isCommunalHubShowing by collectLastValue(underTest.isCommunalHubShowing)
            assertThat(isCommunalHubShowing).isTrue()
        }

    @Test
    fun isCommunalShowing_sceneContainerDisabled_onBlankScene_false() =
        testScope.runTest {
            underTest.setDesiredScene(CommunalSceneKey.Blank)

            val isCommunalHubShowing by collectLastValue(underTest.isCommunalHubShowing)
            assertThat(isCommunalHubShowing).isFalse()
        }

    @Test
    fun isCommunalShowing_sceneContainerEnabled_onCommunalScene_true() =
        testScope.runTest {
            sceneContainerFlags = FakeSceneContainerFlags(enabled = true)
            underTest =
                CommunalRepositoryImpl(
                    featureFlagsClassic,
                    sceneContainerFlags,
                    sceneContainerRepository,
                )

            sceneContainerRepository.setDesiredScene(SceneModel(key = SceneKey.Communal))

            val isCommunalHubShowing by collectLastValue(underTest.isCommunalHubShowing)
            assertThat(isCommunalHubShowing).isTrue()
        }

    @Test
    fun isCommunalShowing_sceneContainerEnabled_onLockscreenScene_false() =
        testScope.runTest {
            sceneContainerFlags = FakeSceneContainerFlags(enabled = true)
            underTest =
                CommunalRepositoryImpl(
                    featureFlagsClassic,
                    sceneContainerFlags,
                    sceneContainerRepository,
                )

            sceneContainerRepository.setDesiredScene(SceneModel(key = SceneKey.Lockscreen))

            val isCommunalHubShowing by collectLastValue(underTest.isCommunalHubShowing)
            assertThat(isCommunalHubShowing).isFalse()
        }
}
+184 −0

File changed and moved.

Preview size limit exceeded, changes collapsed.

+17 −0
Original line number Diff line number Diff line
@@ -21,16 +21,24 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map

/** Encapsulates the state of communal mode. */
interface CommunalRepository {
    /** Whether communal features are enabled. */
    val isCommunalEnabled: Boolean

    /** Whether the communal hub is showing. */
    val isCommunalHubShowing: Flow<Boolean>

    /**
     * Target scene as requested by the underlying [SceneTransitionLayout] or through
     * [setDesiredScene].
@@ -46,6 +54,8 @@ class CommunalRepositoryImpl
@Inject
constructor(
    private val featureFlagsClassic: FeatureFlagsClassic,
    sceneContainerFlags: SceneContainerFlags,
    sceneContainerRepository: SceneContainerRepository,
) : CommunalRepository {
    override val isCommunalEnabled: Boolean
        get() = featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()
@@ -57,4 +67,11 @@ constructor(
    override fun setDesiredScene(desiredScene: CommunalSceneKey) {
        _desiredScene.value = desiredScene
    }

    override val isCommunalHubShowing: Flow<Boolean> =
        if (sceneContainerFlags.isEnabled()) {
            sceneContainerRepository.desiredScene.map { scene -> scene.key == SceneKey.Communal }
        } else {
            desiredScene.map { sceneKey -> sceneKey == CommunalSceneKey.Communal }
        }
}
+71 −9
Original line number Diff line number Diff line
@@ -16,16 +16,24 @@

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

import android.app.smartspace.SmartspaceTarget
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 com.android.systemui.smartspace.data.repository.SmartspaceRepository
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map

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

    /** Whether communal features are enabled. */
@@ -44,14 +55,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 +79,63 @@ 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)
            }
            combine(smartspaceContent, widgetContent) { smartspace, widgets ->
                smartspace + widgets
            }
        }

    /** 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 flow of available smartspace content. Currently only showing timer targets. */
    private val smartspaceContent: Flow<List<CommunalContentModel.Smartspace>> =
        if (!smartspaceRepository.isSmartspaceRemoteViewsEnabled) {
            flowOf(emptyList())
        } else {
            smartspaceRepository.lockscreenSmartspaceTargets.map { targets ->
                targets
                    .filter { target ->
                        target.featureType == SmartspaceTarget.FEATURE_TIMER &&
                            target.remoteViews != null
                    }
                    .map Target@{ target ->
                        return@Target CommunalContentModel.Smartspace(
                            smartspaceTargetId = target.smartspaceTargetId,
                            remoteViews = target.remoteViews!!,
                            // Smartspace always as HALF for now.
                            size = CommunalContentSize.HALF,
                        )
                    }
            }
        }

    /** 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),
        )
}
Loading