Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt +33 −1 Original line number Diff line number Diff line Loading @@ -23,16 +23,27 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.CommunalAppWidgetHostView import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock 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 @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) class CommunalAppWidgetHostTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private lateinit var testableLooper: TestableLooper private lateinit var underTest: CommunalAppWidgetHost Loading @@ -43,9 +54,11 @@ class CommunalAppWidgetHostTest : SysuiTestCase() { underTest = CommunalAppWidgetHost( context = context, backgroundScope = kosmos.applicationCoroutineScope, hostId = 116, interactionHandler = mock(), looper = testableLooper.looper looper = testableLooper.looper, logBuffer = logcatLogBuffer("CommunalAppWidgetHostTest"), ) } Loading @@ -64,4 +77,23 @@ class CommunalAppWidgetHostTest : SysuiTestCase() { assertThat(view).isNotNull() assertThat(view.appWidgetId).isEqualTo(appWidgetId) } @Test fun appWidgetIdToRemove_emit() = testScope.runTest { val appWidgetIdToRemove by collectLastValue(underTest.appWidgetIdToRemove) // Nothing should be emitted yet assertThat(appWidgetIdToRemove).isNull() underTest.onAppWidgetRemoved(appWidgetId = 1) runCurrent() assertThat(appWidgetIdToRemove).isEqualTo(1) underTest.onAppWidgetRemoved(appWidgetId = 2) runCurrent() assertThat(appWidgetIdToRemove).isEqualTo(2) } } packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +44 −0 Original line number Diff line number Diff line Loading @@ -21,14 +21,21 @@ 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.domain.interactor.communalInteractor 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.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 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.flow.MutableSharedFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before Loading @@ -47,6 +54,8 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int> private lateinit var underTest: CommunalAppWidgetHostStartable @Before Loading @@ -54,6 +63,9 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) appWidgetIdToRemove = MutableSharedFlow() whenever(appWidgetHost.appWidgetIdToRemove).thenReturn(appWidgetIdToRemove) underTest = CommunalAppWidgetHostStartable( appWidgetHost, Loading Loading @@ -120,6 +132,38 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { } } @Test fun removeAppWidgetReportedByHost() = with(kosmos) { testScope.runTest { // Set up communal widgets val widget1 = mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(1) } val widget2 = mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(2) } val widget3 = mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(3) } fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3)) underTest.start() // Assert communal widgets has 3 val communalWidgets by collectLastValue(fakeCommunalWidgetRepository.communalWidgets) assertThat(communalWidgets).containsExactly(widget1, widget2, widget3) // Report app widget 1 to remove and assert widget removed appWidgetIdToRemove.emit(1) runCurrent() assertThat(communalWidgets).containsExactly(widget2, widget3) // Report app widget 3 to remove and assert widget removed appWidgetIdToRemove.emit(3) runCurrent() assertThat(communalWidgets).containsExactly(widget2) } } private suspend fun setCommunalAvailable(available: Boolean) = with(kosmos) { fakeKeyguardRepository.setIsEncryptedOrLockdown(false) Loading packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt +12 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.CommunalLog Loading @@ -35,6 +36,7 @@ import dagger.Module import dagger.Provides import java.util.Optional import javax.inject.Named import kotlinx.coroutines.CoroutineScope @Module interface CommunalWidgetRepositoryModule { Loading @@ -52,10 +54,19 @@ interface CommunalWidgetRepositoryModule { @Provides fun provideCommunalAppWidgetHost( @Application context: Context, @Background backgroundScope: CoroutineScope, interactionHandler: WidgetInteractionHandler, @Main looper: Looper, @CommunalLog logBuffer: LogBuffer, ): CommunalAppWidgetHost { return CommunalAppWidgetHost(context, APP_WIDGET_HOST_ID, interactionHandler, looper) return CommunalAppWidgetHost( context, backgroundScope, APP_WIDGET_HOST_ID, interactionHandler, looper, logBuffer, ) } @SysUISingleton Loading packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt +29 −1 Original line number Diff line number Diff line Loading @@ -22,14 +22,31 @@ import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.os.Looper import android.widget.RemoteViews import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch /** Communal app widget host that creates a [CommunalAppWidgetHostView]. */ class CommunalAppWidgetHost( context: Context, private val backgroundScope: CoroutineScope, hostId: Int, interactionHandler: RemoteViews.InteractionHandler, looper: Looper looper: Looper, logBuffer: LogBuffer, ) : AppWidgetHost(context, hostId, interactionHandler, looper) { private val logger = Logger(logBuffer, TAG) private val _appWidgetIdToRemove = MutableSharedFlow<Int>() /** App widget ids that have been removed and no longer available. */ val appWidgetIdToRemove: SharedFlow<Int> = _appWidgetIdToRemove.asSharedFlow() override fun onCreateView( context: Context, appWidgetId: Int, Loading @@ -52,4 +69,15 @@ class CommunalAppWidgetHost( // `createView`, but we are sure that the hostView is `CommunalAppWidgetHostView` return createView(context, appWidgetId, appWidget) as CommunalAppWidgetHostView } override fun onAppWidgetRemoved(appWidgetId: Int) { backgroundScope.launch { logger.i({ "App widget removed from system: $int1" }) { int1 = appWidgetId } _appWidgetIdToRemove.emit(appWidgetId) } } companion object { private const val TAG = "CommunalAppWidgetHost" } } packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt +5 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ constructor( @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. Loading @@ -47,6 +48,10 @@ constructor( .filter { (previous, new) -> previous != new } .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) } .launchIn(bgScope) appWidgetHost.appWidgetIdToRemove .onEach { appWidgetId -> communalInteractor.deleteWidget(appWidgetId) } .launchIn(bgScope) } private suspend fun updateAppWidgetHostActive(active: Boolean) = Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt +33 −1 Original line number Diff line number Diff line Loading @@ -23,16 +23,27 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.CommunalAppWidgetHostView import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock 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 @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) class CommunalAppWidgetHostTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private lateinit var testableLooper: TestableLooper private lateinit var underTest: CommunalAppWidgetHost Loading @@ -43,9 +54,11 @@ class CommunalAppWidgetHostTest : SysuiTestCase() { underTest = CommunalAppWidgetHost( context = context, backgroundScope = kosmos.applicationCoroutineScope, hostId = 116, interactionHandler = mock(), looper = testableLooper.looper looper = testableLooper.looper, logBuffer = logcatLogBuffer("CommunalAppWidgetHostTest"), ) } Loading @@ -64,4 +77,23 @@ class CommunalAppWidgetHostTest : SysuiTestCase() { assertThat(view).isNotNull() assertThat(view.appWidgetId).isEqualTo(appWidgetId) } @Test fun appWidgetIdToRemove_emit() = testScope.runTest { val appWidgetIdToRemove by collectLastValue(underTest.appWidgetIdToRemove) // Nothing should be emitted yet assertThat(appWidgetIdToRemove).isNull() underTest.onAppWidgetRemoved(appWidgetId = 1) runCurrent() assertThat(appWidgetIdToRemove).isEqualTo(1) underTest.onAppWidgetRemoved(appWidgetId = 2) runCurrent() assertThat(appWidgetIdToRemove).isEqualTo(2) } }
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +44 −0 Original line number Diff line number Diff line Loading @@ -21,14 +21,21 @@ 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.domain.interactor.communalInteractor 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.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 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.flow.MutableSharedFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before Loading @@ -47,6 +54,8 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int> private lateinit var underTest: CommunalAppWidgetHostStartable @Before Loading @@ -54,6 +63,9 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) appWidgetIdToRemove = MutableSharedFlow() whenever(appWidgetHost.appWidgetIdToRemove).thenReturn(appWidgetIdToRemove) underTest = CommunalAppWidgetHostStartable( appWidgetHost, Loading Loading @@ -120,6 +132,38 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { } } @Test fun removeAppWidgetReportedByHost() = with(kosmos) { testScope.runTest { // Set up communal widgets val widget1 = mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(1) } val widget2 = mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(2) } val widget3 = mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(3) } fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3)) underTest.start() // Assert communal widgets has 3 val communalWidgets by collectLastValue(fakeCommunalWidgetRepository.communalWidgets) assertThat(communalWidgets).containsExactly(widget1, widget2, widget3) // Report app widget 1 to remove and assert widget removed appWidgetIdToRemove.emit(1) runCurrent() assertThat(communalWidgets).containsExactly(widget2, widget3) // Report app widget 3 to remove and assert widget removed appWidgetIdToRemove.emit(3) runCurrent() assertThat(communalWidgets).containsExactly(widget2) } } private suspend fun setCommunalAvailable(available: Boolean) = with(kosmos) { fakeKeyguardRepository.setIsEncryptedOrLockdown(false) Loading
packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt +12 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.CommunalLog Loading @@ -35,6 +36,7 @@ import dagger.Module import dagger.Provides import java.util.Optional import javax.inject.Named import kotlinx.coroutines.CoroutineScope @Module interface CommunalWidgetRepositoryModule { Loading @@ -52,10 +54,19 @@ interface CommunalWidgetRepositoryModule { @Provides fun provideCommunalAppWidgetHost( @Application context: Context, @Background backgroundScope: CoroutineScope, interactionHandler: WidgetInteractionHandler, @Main looper: Looper, @CommunalLog logBuffer: LogBuffer, ): CommunalAppWidgetHost { return CommunalAppWidgetHost(context, APP_WIDGET_HOST_ID, interactionHandler, looper) return CommunalAppWidgetHost( context, backgroundScope, APP_WIDGET_HOST_ID, interactionHandler, looper, logBuffer, ) } @SysUISingleton Loading
packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt +29 −1 Original line number Diff line number Diff line Loading @@ -22,14 +22,31 @@ import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.os.Looper import android.widget.RemoteViews import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch /** Communal app widget host that creates a [CommunalAppWidgetHostView]. */ class CommunalAppWidgetHost( context: Context, private val backgroundScope: CoroutineScope, hostId: Int, interactionHandler: RemoteViews.InteractionHandler, looper: Looper looper: Looper, logBuffer: LogBuffer, ) : AppWidgetHost(context, hostId, interactionHandler, looper) { private val logger = Logger(logBuffer, TAG) private val _appWidgetIdToRemove = MutableSharedFlow<Int>() /** App widget ids that have been removed and no longer available. */ val appWidgetIdToRemove: SharedFlow<Int> = _appWidgetIdToRemove.asSharedFlow() override fun onCreateView( context: Context, appWidgetId: Int, Loading @@ -52,4 +69,15 @@ class CommunalAppWidgetHost( // `createView`, but we are sure that the hostView is `CommunalAppWidgetHostView` return createView(context, appWidgetId, appWidget) as CommunalAppWidgetHostView } override fun onAppWidgetRemoved(appWidgetId: Int) { backgroundScope.launch { logger.i({ "App widget removed from system: $int1" }) { int1 = appWidgetId } _appWidgetIdToRemove.emit(appWidgetId) } } companion object { private const val TAG = "CommunalAppWidgetHost" } }
packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt +5 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ constructor( @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. Loading @@ -47,6 +48,10 @@ constructor( .filter { (previous, new) -> previous != new } .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) } .launchIn(bgScope) appWidgetHost.appWidgetIdToRemove .onEach { appWidgetId -> communalInteractor.deleteWidget(appWidgetId) } .launchIn(bgScope) } private suspend fun updateAppWidgetHostActive(active: Boolean) = Loading