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

Commit 7c43c899 authored by Darrell Shi's avatar Darrell Shi
Browse files

Remove widget from hub when its package is uninstalled

Test: atest CommunalAppWidgetHostTest
Test: atest CommunalAppWidgetHostStartableTest
Test: verify from UI and logs that uninstalled widgets are properly
removed
Bug: 322170342
Fix: 322170342
Flag: ACONFIG com.android.systemui.communal_hub STAGING

Change-Id: I9cd508c97d8c3a9a2aedf1a98ded7c81e32238dc
parent 37d7dedf
Loading
Loading
Loading
Loading
+33 −1
Original line number Diff line number Diff line
@@ -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
@@ -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"),
            )
    }

@@ -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)
        }
}
+44 −0
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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,
@@ -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)
+12 −1
Original line number Diff line number Diff line
@@ -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
@@ -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 {
@@ -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
+29 −1
Original line number Diff line number Diff line
@@ -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,
@@ -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"
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -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.
@@ -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