diff options
14 files changed, 429 insertions, 167 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index bb3429e72b35..c979ca63950a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -30,7 +30,6 @@ import com.android.systemui.communal.data.db.CommunalWidgetItem import com.android.systemui.communal.shared.CommunalWidgetHost import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.widgets.CommunalAppWidgetHost -import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.communal.widgets.widgetConfiguratorFail import com.android.systemui.communal.widgets.widgetConfiguratorSuccess import com.android.systemui.coroutines.collectLastValue @@ -45,8 +44,7 @@ import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -62,24 +60,17 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class CommunalWidgetRepositoryImplTest : SysuiTestCase() { - @Mock private lateinit var appWidgetManagerOptional: Optional<AppWidgetManager> - @Mock private lateinit var appWidgetManager: AppWidgetManager - @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost - @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo - @Mock private lateinit var providerInfoA: AppWidgetProviderInfo - @Mock private lateinit var communalWidgetHost: CommunalWidgetHost - @Mock private lateinit var communalWidgetDao: CommunalWidgetDao private lateinit var logBuffer: LogBuffer + private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>> private val kosmos = testKosmos() - private val testDispatcher = kosmos.testDispatcher private val testScope = kosmos.testScope private val fakeAllowlist = @@ -94,7 +85,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - + fakeWidgets = MutableStateFlow(emptyMap()) logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest") setAppWidgetIds(emptyList()) @@ -102,13 +93,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray()) whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch") - whenever(communalWidgetDao.getWidgets()).thenReturn(flowOf(emptyMap())) - whenever(appWidgetManagerOptional.isPresent).thenReturn(true) - whenever(appWidgetManagerOptional.get()).thenReturn(appWidgetManager) + whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets) underTest = CommunalWidgetRepositoryImpl( - appWidgetManagerOptional, + Optional.of(appWidgetManager), appWidgetHost, testScope.backgroundScope, kosmos.testDispatcher, @@ -119,30 +108,16 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test - fun neverQueryDbForWidgets_whenHostIsInactive() = - testScope.runTest { - underTest.updateAppWidgetHostActive(false) - underTest.communalWidgets.launchIn(testScope.backgroundScope) - runCurrent() - - verify(communalWidgetDao, never()).getWidgets() - } - - @Test - fun communalWidgets_whenHostIsActive_queryWidgetsFromDb() = + fun communalWidgets_queryWidgetsFromDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1) val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L) - whenever(communalWidgetDao.getWidgets()) - .thenReturn(flowOf(mapOf(communalItemRankEntry to communalWidgetItemEntry))) + fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry) whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA) installedProviders(listOf(stopwatchProviderInfo)) val communalWidgets by collectLastValue(underTest.communalWidgets) - runCurrent() verify(communalWidgetDao).getWidgets() assertThat(communalWidgets) .containsExactly( @@ -157,8 +132,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_allocateId_bindWidget_andAddToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 @@ -176,8 +149,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationFails_doNotAddWidgetToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 @@ -195,23 +166,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationThrowsError_doNotAddWidgetToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) - underTest.addWidget( - provider, - priority, - object : WidgetConfigurator { - override suspend fun configureWidget(appWidgetId: Int): Boolean { - throw IllegalStateException("some error") - } - } - ) + underTest.addWidget(provider, priority) { throw IllegalStateException("some error") } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -222,8 +183,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationNotRequired_doesNotConfigure_addWidgetToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 @@ -241,8 +200,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun deleteWidget_removeWidgetId_andDeleteFromDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val id = 1 underTest.deleteWidget(id) runCurrent() @@ -254,8 +211,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun reorderWidgets_queryDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3) underTest.updateWidgetOrder(widgetIdToPriorityMap) runCurrent() @@ -263,28 +218,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) } - @Test - fun appWidgetHost_startListening() = - testScope.runTest { - verify(appWidgetHost, never()).startListening() - - underTest.updateAppWidgetHostActive(true) - - verify(appWidgetHost).startListening() - } - - @Test - fun appWidgetHost_stopListening() = - testScope.runTest { - underTest.updateAppWidgetHostActive(true) - - verify(appWidgetHost).startListening() - - underTest.updateAppWidgetHostActive(false) - - verify(appWidgetHost).stopListening() - } - private fun installedProviders(providers: List<AppWidgetProviderInfo>) { whenever(appWidgetManager.installedProviders).thenReturn(providers) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt index e8216735fb5d..6a3fc2a060eb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt @@ -80,15 +80,4 @@ class CommunalInteractorCommunalDisabledTest : SysuiTestCase() { assertThat(isCommunalAvailable).isFalse() } - - @Test - fun updateAppWidgetHostActive_whenStorageUnlock_false() = - testScope.runTest { - assertThat(widgetRepository.isHostActive()).isFalse() - - keyguardRepository.setIsEncryptedOrLockdown(false) - runCurrent() - - assertThat(widgetRepository.isHostActive()).isFalse() - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 1b7117f41bbb..a083e7cf22c7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -172,20 +172,6 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun updateAppWidgetHostActive_uponStorageUnlockAsMainUser_true() = - testScope.runTest { - collectLastValue(underTest.isCommunalAvailable) - assertThat(widgetRepository.isHostActive()).isFalse() - - keyguardRepository.setIsEncryptedOrLockdown(false) - userRepository.setSelectedUserInfo(mainUser) - keyguardRepository.setKeyguardShowing(true) - runCurrent() - - assertThat(widgetRepository.isHostActive()).isTrue() - } - - @Test fun widget_tutorialCompletedAndWidgetsAvailable_showWidgetContent() = testScope.runTest { // Keyguard showing, and tutorial completed. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt new file mode 100644 index 000000000000..112b0c797854 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -0,0 +1,132 @@ +/* + * 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.widgets + +import android.content.pm.UserInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalAppWidgetHostStartableTest : SysuiTestCase() { + private val kosmos = testKosmos() + + @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost + + private lateinit var underTest: CommunalAppWidgetHostStartable + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) + + underTest = + CommunalAppWidgetHostStartable( + appWidgetHost, + kosmos.communalInteractor, + kosmos.applicationCoroutineScope, + kosmos.testDispatcher, + ) + } + + @Test + fun editModeShowingStartsAppWidgetHost() = + with(kosmos) { + testScope.runTest { + setCommunalAvailable(false) + communalInteractor.setEditModeOpen(true) + verify(appWidgetHost, never()).startListening() + + underTest.start() + runCurrent() + + verify(appWidgetHost).startListening() + verify(appWidgetHost, never()).stopListening() + + communalInteractor.setEditModeOpen(false) + runCurrent() + + verify(appWidgetHost).stopListening() + } + } + + @Test + fun communalShowingStartsAppWidgetHost() = + with(kosmos) { + testScope.runTest { + setCommunalAvailable(true) + communalInteractor.setEditModeOpen(false) + verify(appWidgetHost, never()).startListening() + + underTest.start() + runCurrent() + + verify(appWidgetHost).startListening() + verify(appWidgetHost, never()).stopListening() + + setCommunalAvailable(false) + runCurrent() + + verify(appWidgetHost).stopListening() + } + } + + @Test + fun communalAndEditModeNotShowingNeverStartListening() = + with(kosmos) { + testScope.runTest { + setCommunalAvailable(false) + communalInteractor.setEditModeOpen(false) + + underTest.start() + runCurrent() + + verify(appWidgetHost, never()).startListening() + verify(appWidgetHost, never()).stopListening() + } + } + + private suspend fun setCommunalAvailable(available: Boolean) = + with(kosmos) { + fakeKeyguardRepository.setIsEncryptedOrLockdown(!available) + fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + fakeKeyguardRepository.setKeyguardShowing(true) + } + + private companion object { + val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt new file mode 100644 index 000000000000..b4a0a37ec9ef --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt @@ -0,0 +1,104 @@ +/* + * 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.util.kotlin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.kotlin.BooleanFlowOperators.and +import com.android.systemui.util.kotlin.BooleanFlowOperators.not +import com.android.systemui.util.kotlin.BooleanFlowOperators.or +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BooleanFlowOperatorsTest : SysuiTestCase() { + + val kosmos = testKosmos() + val testScope = kosmos.testScope + + @Test + fun and_allTrue_returnsTrue() = + testScope.runTest { + val result by collectLastValue(and(TRUE, TRUE)) + assertThat(result).isTrue() + } + + @Test + fun and_anyFalse_returnsFalse() = + testScope.runTest { + val result by collectLastValue(and(TRUE, FALSE, TRUE)) + assertThat(result).isFalse() + } + + @Test + fun and_allFalse_returnsFalse() = + testScope.runTest { + val result by collectLastValue(and(FALSE, FALSE, FALSE)) + assertThat(result).isFalse() + } + + @Test + fun or_allTrue_returnsTrue() = + testScope.runTest { + val result by collectLastValue(or(TRUE, TRUE)) + assertThat(result).isTrue() + } + + @Test + fun or_anyTrue_returnsTrue() = + testScope.runTest { + val result by collectLastValue(or(FALSE, TRUE, FALSE)) + assertThat(result).isTrue() + } + + @Test + fun or_allFalse_returnsFalse() = + testScope.runTest { + val result by collectLastValue(or(FALSE, FALSE, FALSE)) + assertThat(result).isFalse() + } + + @Test + fun not_true_returnsFalse() = + testScope.runTest { + val result by collectLastValue(not(TRUE)) + assertThat(result).isFalse() + } + + @Test + fun not_false_returnsFalse() = + testScope.runTest { + val result by collectLastValue(not(FALSE)) + assertThat(result).isTrue() + } + + private companion object { + val TRUE: Flow<Boolean> + get() = flowOf(true) + val FALSE: Flow<Boolean> + get() = flowOf(false) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index 3287ed4d4991..f36547b01802 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -27,21 +27,17 @@ import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.util.kotlin.getValue import java.util.Optional import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -67,18 +63,15 @@ interface CommunalWidgetRepository { * @param widgetIdToPriorityMap mapping of the widget ids to the priority of the widget. */ fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {} - - /** Update whether the app widget host should be active. */ - fun updateAppWidgetHostActive(active: Boolean) } @SysUISingleton class CommunalWidgetRepositoryImpl @Inject constructor( - private val appWidgetManager: Optional<AppWidgetManager>, + appWidgetManagerOptional: Optional<AppWidgetManager>, private val appWidgetHost: CommunalAppWidgetHost, - @Application private val applicationScope: CoroutineScope, + @Background private val bgScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, private val communalWidgetHost: CommunalWidgetHost, private val communalWidgetDao: CommunalWidgetDao, @@ -90,41 +83,22 @@ constructor( private val logger = Logger(logBuffer, TAG) - override fun updateAppWidgetHostActive(active: Boolean) { - if (active == isHostActive.value) { - return - } - - if (active) { - appWidgetHost.startListening() - } else { - appWidgetHost.stopListening() - } - isHostActive.value = active - } - - private val isHostActive = MutableStateFlow(false) + private val appWidgetManager by appWidgetManagerOptional - @OptIn(ExperimentalCoroutinesApi::class) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = - isHostActive.flatMapLatest { isHostActive -> - if (!isHostActive || !appWidgetManager.isPresent) { - return@flatMapLatest flowOf(emptyList()) - } - communalWidgetDao - .getWidgets() - .map { it.map(::mapToContentModel) } - // As this reads from a database and triggers IPCs to AppWidgetManager, - // it should be executed in the background. - .flowOn(bgDispatcher) - } + communalWidgetDao + .getWidgets() + .map { it.mapNotNull(::mapToContentModel) } + // As this reads from a database and triggers IPCs to AppWidgetManager, + // it should be executed in the background. + .flowOn(bgDispatcher) override fun addWidget( provider: ComponentName, priority: Int, configurator: WidgetConfigurator? ) { - applicationScope.launch(bgDispatcher) { + bgScope.launch { val id = communalWidgetHost.allocateIdAndBindWidget(provider) if (id == null) { logger.e("Failed to allocate widget id to ${provider.flattenToString()}") @@ -170,7 +144,7 @@ constructor( } override fun deleteWidget(widgetId: Int) { - applicationScope.launch(bgDispatcher) { + bgScope.launch { communalWidgetDao.deleteWidgetById(widgetId) appWidgetHost.deleteAppWidgetId(widgetId) logger.i("Deleted widget with id $widgetId.") @@ -178,7 +152,7 @@ constructor( } override fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) { - applicationScope.launch(bgDispatcher) { + bgScope.launch { communalWidgetDao.updateWidgetOrder(widgetIdToPriorityMap) logger.i({ "Updated the order of widget list with ids: $str1." }) { str1 = widgetIdToPriorityMap.toString() @@ -189,11 +163,12 @@ constructor( @WorkerThread private fun mapToContentModel( entry: Map.Entry<CommunalItemRank, CommunalWidgetItem> - ): CommunalWidgetContentModel { + ): CommunalWidgetContentModel? { val (_, widgetId) = entry.value + val providerInfo = appWidgetManager?.getAppWidgetInfo(widgetId) ?: return null return CommunalWidgetContentModel( appWidgetId = widgetId, - providerInfo = appWidgetManager.get().getAppWidgetInfo(widgetId), + providerInfo = providerInfo, priority = entry.key.rank, ) } 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 c36f7fa22c82..28adb77f00e0 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 @@ -37,18 +37,22 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.smartspace.data.repository.SmartspaceRepository import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.kotlin.BooleanFlowOperators.and +import com.android.systemui.util.kotlin.BooleanFlowOperators.not +import com.android.systemui.util.kotlin.BooleanFlowOperators.or import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** Encapsulates business-logic related to communal mode. */ @@ -68,6 +72,11 @@ constructor( private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter ) { + private val _editModeOpen = MutableStateFlow(false) + + /** Whether edit mode is currently open. */ + val editModeOpen: StateFlow<Boolean> = _editModeOpen.asStateFlow() + /** Whether communal features are enabled. */ val isCommunalEnabled: Boolean get() = communalRepository.isCommunalEnabled @@ -80,21 +89,17 @@ constructor( val isCommunalAvailable: StateFlow<Boolean> = flowOf(isCommunalEnabled) .flatMapLatest { enabled -> - if (enabled) - combine( - keyguardInteractor.isEncryptedOrLockdown, - userRepository.selectedUserInfo, - keyguardInteractor.isKeyguardVisible, - keyguardInteractor.isDreaming, - ) { isEncryptedOrLockdown, selectedUserInfo, isKeyguardVisible, isDreaming -> - !isEncryptedOrLockdown && - selectedUserInfo.isMain && - (isKeyguardVisible || isDreaming) - } - else flowOf(false) + if (enabled) { + val isMainUser = userRepository.selectedUserInfo.map { it.isMain } + and( + isMainUser, + not(keyguardInteractor.isEncryptedOrLockdown), + or(keyguardInteractor.isKeyguardVisible, keyguardInteractor.isDreaming), + ) + } else { + flowOf(false) + } } - .distinctUntilChanged() - .onEach { available -> widgetRepository.updateAppWidgetHostActive(available) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), @@ -166,6 +171,10 @@ constructor( communalRepository.setDesiredScene(newScene) } + fun setEditModeOpen(isOpen: Boolean) { + _editModeOpen.value = isOpen + } + /** Show the widget editor Activity. */ fun showWidgetEditor() { editWidgetsActivityStarter.startActivity() diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 237a0c005af5..4b98f1ae4fe8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -75,4 +75,7 @@ constructor( _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } + + /** Sets whether edit mode is currently open */ + fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt new file mode 100644 index 000000000000..586df32e6561 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -0,0 +1,61 @@ +/* + * 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.widgets + +import com.android.systemui.CoreStartable +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.util.kotlin.BooleanFlowOperators.or +import com.android.systemui.util.kotlin.pairwise +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext + +@SysUISingleton +class CommunalAppWidgetHostStartable +@Inject +constructor( + private val appWidgetHost: CommunalAppWidgetHost, + private val communalInteractor: CommunalInteractor, + @Background private val bgScope: CoroutineScope, + @Main private val uiDispatcher: CoroutineDispatcher +) : CoreStartable { + override fun start() { + or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) + // Only trigger updates on state changes, ignoring the initial false value. + .pairwise(false) + .filter { (previous, new) -> previous != new } + .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) } + .launchIn(bgScope) + } + + private suspend fun updateAppWidgetHostActive(active: Boolean) = + // Always ensure this is called on the main/ui thread. + withContext(uiDispatcher) { + if (active) { + appWidgetHost.startListening() + } else { + appWidgetHost.stopListening() + } + } +} 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 c7a14f9eefe1..a2575439e4b2 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -86,6 +86,8 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + communalViewModel.setEditModeOpen(true) + val windowInsetsController = window.decorView.windowInsetsController windowInsetsController?.hide(WindowInsets.Type.systemBars()) window.setDecorFitsSystemWindows(false) @@ -138,13 +140,16 @@ constructor( override fun onStart() { super.onStart() - uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN) } override fun onStop() { super.onStop() - uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE) } + + override fun onDestroy() { + super.onDestroy() + communalViewModel.setEditModeOpen(false) + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 8d82b552fc1e..95233f701bbb 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -25,6 +25,7 @@ import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.BiometricNotificationService import com.android.systemui.clipboardoverlay.ClipboardListener import com.android.systemui.communal.log.CommunalLoggerStartable +import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable import com.android.systemui.controls.dagger.StartControlsStartableModule import com.android.systemui.dagger.qualifiers.PerUser import com.android.systemui.dreams.AssistantAttentionMonitor @@ -324,4 +325,11 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(CommunalLoggerStartable::class) abstract fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(CommunalAppWidgetHostStartable::class) + abstract fun bindCommunalAppWidgetHostStartable( + impl: CommunalAppWidgetHostStartable + ): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt new file mode 100644 index 000000000000..693a835e25d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt @@ -0,0 +1,52 @@ +/* + * 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.util.kotlin + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map + +object BooleanFlowOperators { + /** + * Logical AND operator for boolean flows. Will collect all flows and [combine] them to + * determine the result. + * + * Usage: + * ``` + * val result = and(flow1, flow2) + * ``` + */ + fun and(vararg flows: Flow<Boolean>): Flow<Boolean> = + combine(flows.asIterable()) { values -> values.all { it } } + + /** + * Logical NOT operator for a boolean flow. + * + * Usage: + * ``` + * val negatedFlow = not(flow) + * ``` + */ + fun not(flow: Flow<Boolean>) = flow.map { !it } + + /** + * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to + * determine the result. + */ + fun or(vararg flows: Flow<Boolean>): Flow<Boolean> = + combine(flows.asIterable()) { values -> values.any { it } } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt index c587f2edd601..5150389930a9 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt @@ -17,6 +17,7 @@ package com.android.systemui.util.kotlin import dagger.Lazy +import java.util.Optional import kotlin.reflect.KProperty /** @@ -30,3 +31,16 @@ import kotlin.reflect.KProperty * ``` */ operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = get() + +/** + * Extension operator that allows developers to use [java.util.Optional] as a nullable property + * delegate: + * ```kotlin + * class MyClass @Inject constructor( + * optionalDependency: Optional<Foo>, + * ) { + * val dependency: Foo? by optionalDependency + * } + * ``` + */ +operator fun <T> Optional<T>.getValue(thisRef: Any?, property: KProperty<*>): T? = getOrNull() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index e25e8c099c21..bc7e7af245a6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -39,13 +39,4 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) { _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority)) } - - private var isHostActive = false - override fun updateAppWidgetHostActive(active: Boolean) { - isHostActive = active - } - - fun isHostActive(): Boolean { - return isHostActive - } } |