diff options
9 files changed, 262 insertions, 3 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 3914e6de7432..77b844debb42 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 @@ -18,11 +18,14 @@ package com.android.systemui.communal.ui.compose 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.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid @@ -68,6 +71,7 @@ fun CommunalHub( span = { index -> GridItemSpan(communalContent[index].size.span) }, ) { index -> CommunalContent( + modifier = Modifier.fillMaxHeight().width(Dimensions.CardWidth), model = communalContent[index], deleteOnClick = viewModel::onDeleteWidget, size = @@ -96,6 +100,7 @@ private fun CommunalContent( ) { when (model) { is CommunalContentModel.Widget -> WidgetContent(model, size, deleteOnClick, modifier) + is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier) is CommunalContentModel.Tutorial -> TutorialContent(modifier) } } @@ -131,6 +136,21 @@ private fun WidgetContent( } @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 = {}) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 3016d03eb4c8..524cccf4017d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -16,6 +16,7 @@ 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 @@ -25,10 +26,12 @@ 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.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 @@ -40,6 +43,7 @@ class CommunalInteractor constructor( private val communalRepository: CommunalRepository, private val widgetRepository: CommunalWidgetRepository, + smartspaceRepository: SmartspaceRepository, tutorialInteractor: CommunalTutorialInteractor, private val appWidgetHost: AppWidgetHost, ) { @@ -83,7 +87,9 @@ constructor( if (isTutorialMode) { return@flatMapLatest flowOf(tutorialContent) } - widgetContent + combine(smartspaceContent, widgetContent) { smartspace, widgets -> + smartspace + widgets + } } /** A list of widget content to be displayed in the communal hub. */ @@ -98,6 +104,28 @@ constructor( } } + /** 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( diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt index 525290ae97e5..69382a5a5552 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.communal.domain.model import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetProviderInfo +import android.widget.RemoteViews import com.android.systemui.communal.shared.model.CommunalContentSize /** Encapsulates data for a communal content. */ @@ -44,4 +45,12 @@ sealed interface CommunalContentModel { ) : CommunalContentModel { override val key = "tutorial_$id" } + + class Smartspace( + smartspaceTargetId: String, + val remoteViews: RemoteViews, + override val size: CommunalContentSize, + ) : CommunalContentModel { + override val key = "smartspace_$smartspaceTargetId" + } } diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt index 03be88fc31d9..c59ef2632f15 100644 --- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt @@ -18,13 +18,15 @@ package com.android.systemui.smartspace.dagger import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.smartspace.SmartspacePrecondition import com.android.systemui.smartspace.SmartspaceTargetFilter +import com.android.systemui.smartspace.data.repository.SmartspaceRepositoryModule import com.android.systemui.smartspace.preconditions.LockscreenPrecondition import dagger.Binds import dagger.BindsOptionalOf import dagger.Module import javax.inject.Named -@Module(subcomponents = [SmartspaceViewComponent::class]) +@Module(subcomponents = [SmartspaceViewComponent::class], + includes = [SmartspaceRepositoryModule::class]) abstract class SmartspaceModule { @Module companion object { diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt new file mode 100644 index 000000000000..2fc0ec290a90 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt @@ -0,0 +1,68 @@ +/* + * 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.smartspace.data.repository + +import android.app.smartspace.SmartspaceTarget +import android.os.Parcelable +import android.widget.RemoteViews +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart + +interface SmartspaceRepository { + /** Whether [RemoteViews] are passed through smartspace targets. */ + val isSmartspaceRemoteViewsEnabled: Boolean + + /** Smartspace targets for the lockscreen surface. */ + val lockscreenSmartspaceTargets: Flow<List<SmartspaceTarget>> +} + +@SysUISingleton +class SmartspaceRepositoryImpl +@Inject +constructor( + private val lockscreenSmartspaceController: LockscreenSmartspaceController, +) : SmartspaceRepository, BcSmartspaceDataPlugin.SmartspaceTargetListener { + + override val isSmartspaceRemoteViewsEnabled: Boolean + get() = android.app.smartspace.flags.Flags.remoteViews() + + private val _lockscreenSmartspaceTargets: MutableStateFlow<List<SmartspaceTarget>> = + MutableStateFlow(emptyList()) + override val lockscreenSmartspaceTargets: Flow<List<SmartspaceTarget>> = + _lockscreenSmartspaceTargets + .onStart { + lockscreenSmartspaceController.addListener(listener = this@SmartspaceRepositoryImpl) + } + .onCompletion { + lockscreenSmartspaceController.removeListener( + listener = this@SmartspaceRepositoryImpl + ) + } + + override fun onSmartspaceTargetsUpdated(targetsNullable: MutableList<out Parcelable>?) { + targetsNullable?.let { targets -> + _lockscreenSmartspaceTargets.value = targets.filterIsInstance<SmartspaceTarget>() + } + ?: run { _lockscreenSmartspaceTargets.value = emptyList() } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryModule.kt new file mode 100644 index 000000000000..c77bcc50b69a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryModule.kt @@ -0,0 +1,25 @@ +/* + * 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.smartspace.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface SmartspaceRepositoryModule { + @Binds fun smartspaceRepository(impl: SmartspaceRepositoryImpl): SmartspaceRepository +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 8f563cc632b3..5460a1b8ce9f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -17,7 +17,10 @@ package com.android.systemui.communal.domain.interactor +import android.app.smartspace.SmartspaceTarget import android.provider.Settings +import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED +import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -30,7 +33,9 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -40,6 +45,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations @SmallTest @@ -53,6 +59,7 @@ class CommunalInteractorTest : SysuiTestCase() { private lateinit var tutorialRepository: FakeCommunalTutorialRepository private lateinit var communalRepository: FakeCommunalRepository private lateinit var widgetRepository: FakeCommunalWidgetRepository + private lateinit var smartspaceRepository: FakeSmartspaceRepository private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var underTest: CommunalInteractor @@ -68,6 +75,7 @@ class CommunalInteractorTest : SysuiTestCase() { tutorialRepository = withDeps.tutorialRepository communalRepository = withDeps.communalRepository widgetRepository = withDeps.widgetRepository + smartspaceRepository = withDeps.smartspaceRepository keyguardRepository = withDeps.keyguardRepository underTest = withDeps.communalInteractor @@ -123,7 +131,7 @@ class CommunalInteractorTest : SysuiTestCase() { // Keyguard showing, and tutorial completed. keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) // Widgets are available. val widgets = @@ -156,6 +164,79 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test + fun smartspace_onlyShowTimersWithRemoteViews() = + testScope.runTest { + // Keyguard showing, and tutorial completed. + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setKeyguardOccluded(false) + tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + + // Not a timer + val target1 = mock(SmartspaceTarget::class.java) + whenever(target1.smartspaceTargetId).thenReturn("target1") + whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_WEATHER) + whenever(target1.remoteViews).thenReturn(mock(RemoteViews::class.java)) + + // Does not have RemoteViews + val target2 = mock(SmartspaceTarget::class.java) + whenever(target1.smartspaceTargetId).thenReturn("target2") + whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) + whenever(target1.remoteViews).thenReturn(null) + + // Timer and has RemoteViews + val target3 = mock(SmartspaceTarget::class.java) + whenever(target1.smartspaceTargetId).thenReturn("target3") + whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) + whenever(target1.remoteViews).thenReturn(mock(RemoteViews::class.java)) + + val targets = listOf(target1, target2, target3) + smartspaceRepository.setLockscreenSmartspaceTargets(targets) + + val communalContent by collectLastValue(underTest.communalContent) + assertThat(communalContent?.size).isEqualTo(1) + assertThat(communalContent?.get(0)?.key).isEqualTo("smartspace_target3") + } + + @Test + fun smartspace_smartspaceAndWidgetsAvailable_showSmartspaceAndWidgetContent() = + testScope.runTest { + // Keyguard showing, and tutorial completed. + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setKeyguardOccluded(false) + tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + + // Widgets available. + val widgets = + listOf( + CommunalWidgetContentModel( + appWidgetId = 0, + priority = 30, + providerInfo = mock(), + ), + CommunalWidgetContentModel( + appWidgetId = 1, + priority = 20, + providerInfo = mock(), + ), + ) + widgetRepository.setCommunalWidgets(widgets) + + // Smartspace available. + val target = mock(SmartspaceTarget::class.java) + whenever(target.smartspaceTargetId).thenReturn("target") + whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) + whenever(target.remoteViews).thenReturn(mock(RemoteViews::class.java)) + smartspaceRepository.setLockscreenSmartspaceTargets(listOf(target)) + + val communalContent by collectLastValue(underTest.communalContent) + + assertThat(communalContent?.size).isEqualTo(3) + assertThat(communalContent?.get(0)?.key).isEqualTo("smartspace_target") + assertThat(communalContent?.get(1)?.key).isEqualTo("widget_0") + assertThat(communalContent?.get(2)?.key).isEqualTo("widget_1") + } + + @Test fun listensToSceneChange() = testScope.runTest { var desiredScene = collectLastValue(underTest.desiredScene) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt index 4b7240c1a6ee..6c3882fd70f6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt @@ -23,6 +23,7 @@ import com.android.systemui.communal.data.repository.FakeCommunalTutorialReposit import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.util.mockito.mock import kotlinx.coroutines.test.TestScope @@ -34,6 +35,7 @@ object CommunalInteractorFactory { testScope: TestScope = TestScope(), communalRepository: FakeCommunalRepository = FakeCommunalRepository(), widgetRepository: FakeCommunalWidgetRepository = FakeCommunalWidgetRepository(), + smartspaceRepository: FakeSmartspaceRepository = FakeSmartspaceRepository(), tutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(), appWidgetHost: AppWidgetHost = mock(), ): WithDependencies { @@ -46,6 +48,7 @@ object CommunalInteractorFactory { return WithDependencies( communalRepository, widgetRepository, + smartspaceRepository, tutorialRepository, withDeps.keyguardRepository, withDeps.keyguardInteractor, @@ -54,6 +57,7 @@ object CommunalInteractorFactory { CommunalInteractor( communalRepository, widgetRepository, + smartspaceRepository, withDeps.communalTutorialInteractor, appWidgetHost, ), @@ -63,6 +67,7 @@ object CommunalInteractorFactory { data class WithDependencies( val communalRepository: FakeCommunalRepository, val widgetRepository: FakeCommunalWidgetRepository, + val smartspaceRepository: FakeSmartspaceRepository, val tutorialRepository: FakeCommunalTutorialRepository, val keyguardRepository: FakeKeyguardRepository, val keyguardInteractor: KeyguardInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/FakeSmartspaceRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/FakeSmartspaceRepository.kt new file mode 100644 index 000000000000..c8013ef96fa7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/FakeSmartspaceRepository.kt @@ -0,0 +1,21 @@ +package com.android.systemui.smartspace.data.repository + +import android.app.smartspace.SmartspaceTarget +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeSmartspaceRepository( + smartspaceRemoteViewsEnabled: Boolean = true, +) : SmartspaceRepository { + + override val isSmartspaceRemoteViewsEnabled = smartspaceRemoteViewsEnabled + + private val _lockscreenSmartspaceTargets: MutableStateFlow<List<SmartspaceTarget>> = + MutableStateFlow(emptyList()) + override val lockscreenSmartspaceTargets: Flow<List<SmartspaceTarget>> = + _lockscreenSmartspaceTargets + + fun setLockscreenSmartspaceTargets(targets: List<SmartspaceTarget>) { + _lockscreenSmartspaceTargets.value = targets + } +} |