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

Commit 4185ad40 authored by Darrell Shi's avatar Darrell Shi Committed by Android (Google) Code Review
Browse files

Merge "Remove widget from hub when its package is uninstalled" into main

parents fed1de17 7c43c899
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