diff options
15 files changed, 296 insertions, 211 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index b70486473672..c073b79ba5a3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -67,8 +67,9 @@ fun CommunalContainer( // Don't show hub mode UI if keyguard is not present. This is important since we're in the // shade, which can be opened from many locations. val isKeyguardShowing by viewModel.isKeyguardVisible.collectAsState(initial = false) + val isCommunalAvailable by viewModel.isCommunalAvailable.collectAsState() - if (!isKeyguardShowing) { + if (!isKeyguardShowing || !isCommunalAvailable) { return } 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 c0cccba6dff4..fdb17c298e17 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 @@ -19,10 +19,6 @@ package com.android.systemui.communal.data.repository import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName -import android.content.Intent -import android.content.Intent.ACTION_USER_UNLOCKED -import android.os.UserHandle -import android.os.UserManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -38,7 +34,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.log.LogBuffer import com.android.systemui.log.logcatLogBuffer import com.android.systemui.res.R -import com.android.systemui.settings.UserTracker import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever @@ -68,12 +63,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost - @Mock private lateinit var userManager: UserManager - - @Mock private lateinit var userHandle: UserHandle - - @Mock private lateinit var userTracker: UserTracker - @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo @Mock private lateinit var providerInfoA: AppWidgetProviderInfo @@ -86,8 +75,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { private val testScope = kosmos.testScope - private lateinit var communalRepository: FakeCommunalRepository - private lateinit var logBuffer: LogBuffer private val fakeAllowlist = @@ -97,68 +84,60 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { "com.android.fake/WidgetProviderC", ) + private lateinit var underTest: CommunalWidgetRepositoryImpl + @Before fun setUp() { MockitoAnnotations.initMocks(this) logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest") - communalRepository = kosmos.fakeCommunalRepository - communalEnabled(true) setAppWidgetIds(emptyList()) overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray()) whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch") - whenever(userTracker.userHandle).thenReturn(userHandle) whenever(communalWidgetDao.getWidgets()).thenReturn(flowOf(emptyMap())) whenever(appWidgetManagerOptional.isPresent).thenReturn(true) whenever(appWidgetManagerOptional.get()).thenReturn(appWidgetManager) + + underTest = + CommunalWidgetRepositoryImpl( + appWidgetManagerOptional, + appWidgetHost, + testScope.backgroundScope, + kosmos.testDispatcher, + communalWidgetHost, + communalWidgetDao, + logBuffer, + ) } @Test - fun neverQueryDbForWidgets_whenFeatureIsDisabled() = + fun neverQueryDbForWidgets_whenHostIsInactive() = testScope.runTest { - communalEnabled(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) + underTest.updateAppWidgetHostActive(false) + underTest.communalWidgets.launchIn(testScope.backgroundScope) runCurrent() verify(communalWidgetDao, never()).getWidgets() } @Test - fun neverQueryDbForWidgets_whenFeatureEnabled_andUserLocked() = + fun communalWidgets_whenHostIsActive_queryWidgetsFromDb() = testScope.runTest { - userUnlocked(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) - runCurrent() - - verify(communalWidgetDao, never()).getWidgets() - } + underTest.updateAppWidgetHostActive(true) - @Test - fun communalWidgets_whenUserUnlocked_queryWidgetsFromDb() = - testScope.runTest { - userUnlocked(false) - val repository = initCommunalWidgetRepository() - val communalWidgets by collectLastValue(repository.communalWidgets) - runCurrent() 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))) whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA) - userUnlocked(true) installedProviders(listOf(stopwatchProviderInfo)) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - context, - Intent(ACTION_USER_UNLOCKED) - ) - runCurrent() + val communalWidgets by collectLastValue(underTest.communalWidgets) + runCurrent() verify(communalWidgetDao).getWidgets() assertThat(communalWidgets) .containsExactly( @@ -173,9 +152,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_allocateId_bindWidget_andAddToDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val provider = ComponentName("pkg_name", "cls_name") val id = 1 @@ -183,7 +160,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) .thenReturn(id) - repository.addWidget(provider, priority) { true } + underTest.addWidget(provider, priority) { true } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -193,16 +170,14 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationFails_doNotAddWidgetToDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) - repository.addWidget(provider, priority) { false } + underTest.addWidget(provider, priority) { false } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -213,16 +188,14 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationThrowsError_doNotAddWidgetToDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) - repository.addWidget(provider, priority) { throw IllegalStateException("some error") } + underTest.addWidget(provider, priority) { throw IllegalStateException("some error") } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -233,9 +206,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationNotRequired_doesNotConfigure_addWidgetToDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val provider = ComponentName("pkg_name", "cls_name") val id = 1 @@ -244,7 +215,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) .thenReturn(id) var configured = false - repository.addWidget(provider, priority) { + underTest.addWidget(provider, priority) { configured = true true } @@ -258,12 +229,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun deleteWidget_removeWidgetId_andDeleteFromDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val id = 1 - repository.deleteWidget(id) + underTest.deleteWidget(id) runCurrent() verify(communalWidgetDao).deleteWidgetById(id) @@ -273,108 +242,37 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun reorderWidgets_queryDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3) - repository.updateWidgetOrder(widgetIdToPriorityMap) + underTest.updateWidgetOrder(widgetIdToPriorityMap) runCurrent() verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) } @Test - fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = + fun appWidgetHost_startListening() = testScope.runTest { - communalEnabled(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) - runCurrent() - assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(0) - } - - @Test - fun broadcastReceiver_featureEnabledAndUserLocked_registerBroadcastReceiver() = - testScope.runTest { - userUnlocked(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) - runCurrent() - assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(1) - } - - @Test - fun appWidgetHost_userUnlocked_startListening() = - testScope.runTest { - userUnlocked(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) - runCurrent() verify(appWidgetHost, never()).startListening() - userUnlocked(true) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - context, - Intent(ACTION_USER_UNLOCKED) - ) - runCurrent() + underTest.updateAppWidgetHostActive(true) verify(appWidgetHost).startListening() } @Test - fun appWidgetHost_userLockedAgain_stopListening() = + fun appWidgetHost_stopListening() = testScope.runTest { - userUnlocked(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) - runCurrent() - - userUnlocked(true) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - context, - Intent(ACTION_USER_UNLOCKED) - ) - runCurrent() + underTest.updateAppWidgetHostActive(true) verify(appWidgetHost).startListening() - verify(appWidgetHost, never()).stopListening() - userUnlocked(false) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - context, - Intent(ACTION_USER_UNLOCKED) - ) - runCurrent() + underTest.updateAppWidgetHostActive(false) verify(appWidgetHost).stopListening() } - private fun initCommunalWidgetRepository(): CommunalWidgetRepositoryImpl { - return CommunalWidgetRepositoryImpl( - appWidgetManagerOptional, - appWidgetHost, - testScope.backgroundScope, - kosmos.testDispatcher, - fakeBroadcastDispatcher, - communalRepository, - communalWidgetHost, - communalWidgetDao, - userManager, - userTracker, - logBuffer, - ) - } - - private fun communalEnabled(enabled: Boolean) { - communalRepository.setIsCommunalEnabled(enabled) - } - - private fun userUnlocked(userUnlocked: Boolean) { - whenever(userManager.isUserUnlockingOrUnlocked(userHandle)).thenReturn(userUnlocked) - } - 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 new file mode 100644 index 000000000000..ed29aa4ac202 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt @@ -0,0 +1,95 @@ +/* + * 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.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.FakeCommunalRepository +import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.communalInteractor +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +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 + +/** + * This class is a variation of the [CommunalInteractorTest] for cases where communal is disabled. + */ +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class CommunalInteractorCommunalDisabledTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var communalRepository: FakeCommunalRepository + private lateinit var widgetRepository: FakeCommunalWidgetRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + + private lateinit var underTest: CommunalInteractor + + @Before + fun setUp() { + communalRepository = kosmos.fakeCommunalRepository + widgetRepository = kosmos.fakeCommunalWidgetRepository + keyguardRepository = kosmos.fakeKeyguardRepository + + communalRepository.setIsCommunalEnabled(false) + + underTest = kosmos.communalInteractor + } + + @Test + fun isCommunalEnabled_false() = + testScope.runTest { assertThat(underTest.isCommunalEnabled).isFalse() } + + @Test + fun isCommunalAvailable_whenStorageUnlock_false() = + testScope.runTest { + val isCommunalAvailable by collectLastValue(underTest.isCommunalAvailable) + + assertThat(isCommunalAvailable).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + runCurrent() + + 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 b62441f72e98..7769223c8af2 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 @@ -62,6 +62,10 @@ import org.junit.runner.RunWith import org.mockito.Mockito.mock import org.mockito.Mockito.verify +/** + * This class of test cases assume that communal is enabled. For disabled cases, see + * [CommunalInteractorCommunalDisabledTest]. + */ @SmallTest @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @@ -95,17 +99,43 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun communalEnabled() = + fun communalEnabled_true() = + testScope.runTest { assertThat(underTest.isCommunalEnabled).isTrue() } + + @Test + fun isCommunalAvailable_trueWhenStorageUnlock() = + testScope.runTest { + val isAvailable by collectLastValue(underTest.isCommunalAvailable) + assertThat(isAvailable).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + runCurrent() + + assertThat(isAvailable).isTrue() + } + + @Test + fun isCommunalAvailable_whenStorageUnlock_true() = testScope.runTest { - communalRepository.setIsCommunalEnabled(true) - assertThat(underTest.isCommunalEnabled).isTrue() + val isAvailable by collectLastValue(underTest.isCommunalAvailable) + assertThat(isAvailable).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + runCurrent() + + assertThat(isAvailable).isTrue() } @Test - fun communalDisabled() = + fun updateAppWidgetHostActive_uponStorageUnlock_true() = testScope.runTest { - communalRepository.setIsCommunalEnabled(false) - assertThat(underTest.isCommunalEnabled).isFalse() + collectLastValue(underTest.isCommunalAvailable) + assertThat(widgetRepository.isHostActive()).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + runCurrent() + + assertThat(widgetRepository.isHostActive()).isTrue() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index c4ebbdcc2f58..6f62afc560fe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -52,6 +53,8 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.verify @@ -68,6 +71,8 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var authController: AuthController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController + @Mock private lateinit var userTracker: UserTracker + @Captor private lateinit var updateCallbackCaptor: ArgumentCaptor<KeyguardUpdateMonitorCallback> private val mainDispatcher = StandardTestDispatcher() private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -93,6 +98,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { testScope.backgroundScope, systemClock, facePropertyRepository, + userTracker, ) } @@ -553,4 +559,25 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { job.cancel() } + + @Test + fun isEncryptedOrLockdown() = + testScope.runTest { + whenever(userTracker.userId).thenReturn(0) + whenever(keyguardUpdateMonitor.isEncryptedOrLockdown(0)).thenReturn(true) + + // Default value for isEncryptedOrLockdown is true + val isEncryptedOrLockdown by collectLastValue(underTest.isEncryptedOrLockdown) + assertThat(isEncryptedOrLockdown).isTrue() + + verify(keyguardUpdateMonitor).registerCallback(updateCallbackCaptor.capture()) + val updateCallback = updateCallbackCaptor.value + + // Strong auth state updated + whenever(keyguardUpdateMonitor.isEncryptedOrLockdown(0)).thenReturn(false) + updateCallback.onStrongAuthStateChanged(0) + + // Verify no longer encrypted or lockdown + assertThat(isEncryptedOrLockdown).isFalse() + } } 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 bfc5019801d0..1c362e993509 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 @@ -18,11 +18,7 @@ package com.android.systemui.communal.data.repository import android.appwidget.AppWidgetManager import android.content.ComponentName -import android.content.Intent -import android.content.IntentFilter -import android.os.UserManager import androidx.annotation.WorkerThread -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.communal.data.db.CommunalItemRank import com.android.systemui.communal.data.db.CommunalWidgetDao import com.android.systemui.communal.data.db.CommunalWidgetItem @@ -35,7 +31,6 @@ 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.settings.UserTracker import java.util.Optional import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException @@ -43,16 +38,12 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.emptyFlow +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.flow.mapLatest -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext /** Encapsulates the state of widgets for communal mode. */ interface CommunalWidgetRepository { @@ -75,9 +66,11 @@ 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) } -@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class CommunalWidgetRepositoryImpl @Inject @@ -86,12 +79,8 @@ constructor( private val appWidgetHost: CommunalAppWidgetHost, @Application private val applicationScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, - broadcastDispatcher: BroadcastDispatcher, - communalRepository: CommunalRepository, private val communalWidgetHost: CommunalWidgetHost, private val communalWidgetDao: CommunalWidgetDao, - private val userManager: UserManager, - private val userTracker: UserTracker, @CommunalLog logBuffer: LogBuffer, ) : CommunalWidgetRepository { companion object { @@ -100,40 +89,22 @@ constructor( private val logger = Logger(logBuffer, TAG) - // Whether the [AppWidgetHost] is listening for updates. - private var isHostListening = false - - private suspend fun isUserUnlockingOrUnlocked(): Boolean = - withContext(bgDispatcher) { userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) } - - private val isUserUnlocked: Flow<Boolean> = - flowOf(communalRepository.isCommunalEnabled) - .flatMapLatest { enabled -> - if (enabled) { - broadcastDispatcher - .broadcastFlow( - filter = IntentFilter(Intent.ACTION_USER_UNLOCKED), - user = userTracker.userHandle - ) - .onStart { emit(Unit) } - .mapLatest { isUserUnlockingOrUnlocked() } - } else { - emptyFlow() - } - } - .distinctUntilChanged() - - private val isHostActive: Flow<Boolean> = - isUserUnlocked.map { - if (it) { - startListening() - true - } else { - stopListening() - false - } + 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) + @OptIn(ExperimentalCoroutinesApi::class) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = isHostActive.flatMapLatest { isHostActive -> if (!isHostActive || !appWidgetManager.isPresent) { @@ -218,22 +189,4 @@ constructor( priority = entry.key.rank, ) } - - private fun startListening() { - if (isHostListening) { - return - } - - appWidgetHost.startListening() - isHostListening = true - } - - private fun stopListening() { - if (!isHostListening) { - return - } - - appWidgetHost.stopListening() - isHostListening = false - } } 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 aa4a9d0d9569..71523b9e750f 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 @@ -32,22 +32,30 @@ import com.android.systemui.communal.shared.model.ObservableCommunalTransitionSt import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.smartspace.data.repository.SmartspaceRepository import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow 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. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class CommunalInteractor @Inject constructor( + @Application private val applicationScope: CoroutineScope, private val communalRepository: CommunalRepository, private val widgetRepository: CommunalWidgetRepository, private val communalPrefsRepository: CommunalPrefsRepository, @@ -62,6 +70,20 @@ constructor( val isCommunalEnabled: Boolean get() = communalRepository.isCommunalEnabled + /** Whether communal features are enabled and available. */ + val isCommunalAvailable: StateFlow<Boolean> = + flowOf(isCommunalEnabled) + .flatMapLatest { enabled -> + if (enabled) keyguardInteractor.isEncryptedOrLockdown.map { !it } else flowOf(false) + } + .distinctUntilChanged() + .onEach { available -> widgetRepository.updateAppWidgetHostActive(available) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + /** * Target scene as requested by the underlying [SceneTransitionLayout] or through * [onSceneChanged]. diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 4da348e6a92a..73bb0b0ec8a3 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -33,6 +33,8 @@ abstract class BaseCommunalViewModel( private val communalInteractor: CommunalInteractor, val mediaHost: MediaHost, ) { + val isCommunalAvailable: StateFlow<Boolean> = communalInteractor.isCommunalAvailable + val isKeyguardVisible: Flow<Boolean> = communalInteractor.isKeyguardVisible val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 704ebdd40af6..d012d24b767e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -40,11 +40,13 @@ import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardDone import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -54,7 +56,10 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** Defines interface for classes that encapsulate application state for the keyguard. */ @@ -208,6 +213,12 @@ interface KeyguardRepository { val clockShouldBeCentered: Flow<Boolean> /** + * Whether the primary authentication is required for the given user due to lockdown or + * encryption after reboot. + */ + val isEncryptedOrLockdown: Flow<Boolean> + + /** * Returns `true` if the keyguard is showing; `false` otherwise. * * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in @@ -279,6 +290,7 @@ constructor( @Application private val scope: CoroutineScope, private val systemClock: SystemClock, facePropertyRepository: FacePropertyRepository, + private val userTracker: UserTracker, ) : KeyguardRepository { private val _dismissAction: MutableStateFlow<DismissAction> = MutableStateFlow(DismissAction.None) @@ -544,6 +556,24 @@ constructor( awaitClose { dozeTransitionListener.removeCallback(callback) } } + @OptIn(ExperimentalCoroutinesApi::class) + override val isEncryptedOrLockdown: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onStrongAuthStateChanged(userId: Int) { + trySend(userId) + } + } + + keyguardUpdateMonitor.registerCallback(callback) + + awaitClose { keyguardUpdateMonitor.removeCallback(callback) } + } + .filter { userId -> userId == userTracker.userId } + .onStart { emit(userTracker.userId) } + .mapLatest { userId -> keyguardUpdateMonitor.isEncryptedOrLockdown(userId) } + override fun isKeyguardShowing(): Boolean { return keyguardStateController.isShowing } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 6eb3b64d4c09..6170356d4a90 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -264,6 +264,12 @@ constructor( } } + /** + * Whether the primary authentication is required for the given user due to lockdown or + * encryption after reboot. + */ + val isEncryptedOrLockdown: Flow<Boolean> = repository.isEncryptedOrLockdown + fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> { return dozeTransitionModel.filter { states.contains(it.to) } } 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 397dc1a464bd..d0c2d4b82fb3 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 @@ -35,4 +35,13 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : } } } + + private var isHostActive = false + override fun updateAppWidgetHostActive(active: Boolean) { + isHostActive = active + } + + fun isHostActive(): Boolean { + return isHostActive + } } 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 126bd6f03cca..1753ca05347a 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 @@ -67,6 +67,7 @@ object CommunalInteractorFactory { appWidgetHost, editWidgetsActivityStarter, CommunalInteractor( + testScope.backgroundScope, communalRepository, widgetRepository, communalPrefsRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index 7cbbaabe9dfa..65579a6d9ddf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -23,11 +23,13 @@ import com.android.systemui.communal.data.repository.communalWidgetRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.smartspace.data.repository.smartspaceRepository import com.android.systemui.util.mockito.mock val Kosmos.communalInteractor by Fixture { CommunalInteractor( + applicationScope = applicationCoroutineScope, communalRepository = communalRepository, widgetRepository = communalWidgetRepository, mediaRepository = communalMediaRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 975db3b179ac..59f56dd18f0e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -127,6 +127,9 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override val ambientIndicationVisible: MutableStateFlow<Boolean> = MutableStateFlow(false) + private val _isEncryptedOrLockdown = MutableStateFlow(true) + override val isEncryptedOrLockdown: Flow<Boolean> = _isEncryptedOrLockdown + override fun setQuickSettingsVisible(isVisible: Boolean) { _isQuickSettingsVisible.value = isVisible } @@ -247,6 +250,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override fun setKeyguardAlpha(alpha: Float) { _keyguardAlpha.value = alpha } + + fun setIsEncryptedOrLockdown(value: Boolean) { + _isEncryptedOrLockdown.value = value + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt index 565ce2b41521..d8f0cec54596 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt @@ -24,12 +24,14 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope import com.android.systemui.smartspace.data.repository.smartspaceRepository import org.mockito.Mockito.mock val Kosmos.communalInteractor by Kosmos.Fixture { CommunalInteractor( + applicationScope = testScope.backgroundScope, communalRepository = communalRepository, widgetRepository = communalWidgetRepository, mediaRepository = communalMediaRepository, |