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

Commit 59975e3f authored by Darrell Shi's avatar Darrell Shi
Browse files

Handle provider info updates

The CommunalWidgetHost now produces a flow of AppWidgetProviderInfos
that contains the widgets bound to the host. The host also keeps a
listener for each widget and updates this flow when a provider info has
changed.

Test: atest CommunalWidgetHostTest
Test: atest CommunalAppWidgetHostTest
Test: atest CommunalWidgetRepositoryImplTest
Test: atest CommunalAppWidgetHostStartableTest
Bug: 330945453
Flag: ACONFIG com.android.systemui.communal_hub TEAMFOOD
Change-Id: I5a6477195fc351b623fac6194b3448d90f348315
parent 74ee1139
Loading
Loading
Loading
Loading
+101 −14
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.systemui.communal.data.repository

import android.app.backup.BackupManager
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
@@ -49,7 +48,6 @@ import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
@@ -58,7 +56,6 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
@@ -68,10 +65,10 @@ import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
    @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 providerInfoB: AppWidgetProviderInfo
    @Mock private lateinit var providerInfoC: AppWidgetProviderInfo
    @Mock private lateinit var communalWidgetHost: CommunalWidgetHost
    @Mock private lateinit var communalWidgetDao: CommunalWidgetDao
    @Mock private lateinit var backupManager: BackupManager
@@ -79,6 +76,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
    private lateinit var backupUtils: CommunalBackupUtils
    private lateinit var logBuffer: LogBuffer
    private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>>
    private lateinit var fakeProviders: MutableStateFlow<Map<Int, AppWidgetProviderInfo?>>

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
@@ -96,6 +94,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        fakeWidgets = MutableStateFlow(emptyMap())
        fakeProviders = MutableStateFlow(emptyMap())
        logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest")
        backupUtils = CommunalBackupUtils(kosmos.applicationContext)

@@ -103,12 +102,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {

        overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray())

        whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch")
        whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets)
        whenever(communalWidgetHost.appWidgetProviders).thenReturn(fakeProviders)

        underTest =
            CommunalWidgetRepositoryImpl(
                Optional.of(appWidgetManager),
                appWidgetHost,
                testScope.backgroundScope,
                kosmos.testDispatcher,
@@ -126,9 +124,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
            val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1)
            val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L)
            fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry)
            whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA)

            installedProviders(listOf(stopwatchProviderInfo))
            fakeProviders.value = mapOf(1 to providerInfoA)

            val communalWidgets by collectLastValue(underTest.communalWidgets)
            verify(communalWidgetDao).getWidgets()
@@ -145,6 +141,101 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
            verify(backupManager, never()).dataChanged()
        }

    @Test
    fun communalWidgets_widgetsWithoutMatchingProvidersAreSkipped() =
        testScope.runTest {
            // Set up 4 widgets, but widget 3 and 4 don't have matching providers
            fakeWidgets.value =
                mapOf(
                    CommunalItemRank(uid = 1L, rank = 1) to
                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
                    CommunalItemRank(uid = 2L, rank = 2) to
                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
                    CommunalItemRank(uid = 3L, rank = 3) to
                        CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L),
                    CommunalItemRank(uid = 4L, rank = 4) to
                        CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L),
                )
            fakeProviders.value =
                mapOf(
                    1 to providerInfoA,
                    2 to providerInfoB,
                )

            // Expect to see only widget 1 and 2
            val communalWidgets by collectLastValue(underTest.communalWidgets)
            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel(
                        appWidgetId = 1,
                        providerInfo = providerInfoA,
                        priority = 1,
                    ),
                    CommunalWidgetContentModel(
                        appWidgetId = 2,
                        providerInfo = providerInfoB,
                        priority = 2,
                    ),
                )
        }

    @Test
    fun communalWidgets_updatedWhenProvidersUpdate() =
        testScope.runTest {
            // Set up widgets and providers
            fakeWidgets.value =
                mapOf(
                    CommunalItemRank(uid = 1L, rank = 1) to
                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
                    CommunalItemRank(uid = 2L, rank = 2) to
                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
                )
            fakeProviders.value =
                mapOf(
                    1 to providerInfoA,
                    2 to providerInfoB,
                )

            // Expect two widgets
            val communalWidgets by collectLastValue(underTest.communalWidgets)
            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel(
                        appWidgetId = 1,
                        providerInfo = providerInfoA,
                        priority = 1,
                    ),
                    CommunalWidgetContentModel(
                        appWidgetId = 2,
                        providerInfo = providerInfoB,
                        priority = 2,
                    ),
                )

            // Provider info updated for widget 1
            fakeProviders.value =
                mapOf(
                    1 to providerInfoC,
                    2 to providerInfoB,
                )
            runCurrent()

            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel(
                        appWidgetId = 1,
                        // Verify that provider info updated
                        providerInfo = providerInfoC,
                        priority = 1,
                    ),
                    CommunalWidgetContentModel(
                        appWidgetId = 2,
                        providerInfo = providerInfoB,
                        priority = 2,
                    ),
                )
        }

    @Test
    fun addWidget_allocateId_bindWidget_andAddToDb() =
        testScope.runTest {
@@ -434,10 +525,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
            assertThat(restoredWidget2.rank).isEqualTo(expectedWidget2.rank)
        }

    private fun installedProviders(providers: List<AppWidgetProviderInfo>) {
        whenever(appWidgetManager.installedProviders).thenReturn(providers)
    }

    private fun setAppWidgetIds(ids: List<Int>) {
        whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray())
    }
+138 −0
Original line number Diff line number Diff line
@@ -28,6 +28,8 @@ 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.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -36,6 +38,9 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.verify

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -96,4 +101,137 @@ class CommunalAppWidgetHostTest : SysuiTestCase() {

            assertThat(appWidgetIdToRemove).isEqualTo(2)
        }

    @Test
    fun observer_onHostStartListeningTriggeredWhileObserverActive() =
        testScope.runTest {
            // Observer added
            val observer = mock<CommunalAppWidgetHost.Observer>()
            underTest.addObserver(observer)
            runCurrent()

            // Verify callback triggered
            verify(observer, never()).onHostStartListening()
            underTest.startListening()
            runCurrent()
            verify(observer).onHostStartListening()

            clearInvocations(observer)

            // Observer removed
            underTest.removeObserver(observer)
            runCurrent()

            // Verify callback not triggered
            underTest.startListening()
            runCurrent()
            verify(observer, never()).onHostStartListening()
        }

    @Test
    fun observer_onHostStopListeningTriggeredWhileObserverActive() =
        testScope.runTest {
            // Observer added
            val observer = mock<CommunalAppWidgetHost.Observer>()
            underTest.addObserver(observer)
            runCurrent()

            // Verify callback triggered
            verify(observer, never()).onHostStopListening()
            underTest.stopListening()
            runCurrent()
            verify(observer).onHostStopListening()

            clearInvocations(observer)

            // Observer removed
            underTest.removeObserver(observer)
            runCurrent()

            // Verify callback not triggered
            underTest.stopListening()
            runCurrent()
            verify(observer, never()).onHostStopListening()
        }

    @Test
    fun observer_onAllocateAppWidgetIdTriggeredWhileObserverActive() =
        testScope.runTest {
            // Observer added
            val observer = mock<CommunalAppWidgetHost.Observer>()
            underTest.addObserver(observer)
            runCurrent()

            // Verify callback triggered
            verify(observer, never()).onAllocateAppWidgetId(any())
            val id = underTest.allocateAppWidgetId()
            runCurrent()
            verify(observer).onAllocateAppWidgetId(eq(id))

            clearInvocations(observer)

            // Observer removed
            underTest.removeObserver(observer)
            runCurrent()

            // Verify callback not triggered
            underTest.allocateAppWidgetId()
            runCurrent()
            verify(observer, never()).onAllocateAppWidgetId(any())
        }

    @Test
    fun observer_onDeleteAppWidgetIdTriggeredWhileObserverActive() =
        testScope.runTest {
            // Observer added
            val observer = mock<CommunalAppWidgetHost.Observer>()
            underTest.addObserver(observer)
            runCurrent()

            // Verify callback triggered
            verify(observer, never()).onDeleteAppWidgetId(any())
            underTest.deleteAppWidgetId(1)
            runCurrent()
            verify(observer).onDeleteAppWidgetId(eq(1))

            clearInvocations(observer)

            // Observer removed
            underTest.removeObserver(observer)
            runCurrent()

            // Verify callback not triggered
            underTest.deleteAppWidgetId(2)
            runCurrent()
            verify(observer, never()).onDeleteAppWidgetId(any())
        }

    @Test
    fun observer_multipleObservers() =
        testScope.runTest {
            // Set up two observers
            val observer1 = mock<CommunalAppWidgetHost.Observer>()
            val observer2 = mock<CommunalAppWidgetHost.Observer>()
            underTest.addObserver(observer1)
            underTest.addObserver(observer2)
            runCurrent()

            // Verify both observers triggered
            verify(observer1, never()).onHostStartListening()
            verify(observer2, never()).onHostStartListening()
            underTest.startListening()
            runCurrent()
            verify(observer1).onHostStartListening()
            verify(observer2).onHostStartListening()

            // Observer 1 removed
            underTest.removeObserver(observer1)
            runCurrent()

            // Verify only observer 2 is triggered
            underTest.stopListening()
            runCurrent()
            verify(observer2).onHostStopListening()
            verify(observer1, never()).onHostStopListening()
        }
}
+24 −0
Original line number Diff line number Diff line
@@ -60,6 +60,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
    private val kosmos = testKosmos()

    @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
    @Mock private lateinit var communalWidgetHost: CommunalWidgetHost

    private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int>

@@ -78,6 +79,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
        underTest =
            CommunalAppWidgetHostStartable(
                appWidgetHost,
                communalWidgetHost,
                kosmos.communalInteractor,
                kosmos.fakeUserTracker,
                kosmos.applicationCoroutineScope,
@@ -142,6 +144,28 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
            }
        }

    @Test
    fun observeHostWhenCommunalIsAvailable() =
        with(kosmos) {
            testScope.runTest {
                setCommunalAvailable(true)
                communalInteractor.setEditModeOpen(false)
                verify(communalWidgetHost, never()).startObservingHost()
                verify(communalWidgetHost, never()).stopObservingHost()

                underTest.start()
                runCurrent()

                verify(communalWidgetHost).startObservingHost()
                verify(communalWidgetHost, never()).stopObservingHost()

                setCommunalAvailable(false)
                runCurrent()

                verify(communalWidgetHost).stopObservingHost()
            }
        }

    @Test
    fun removeAppWidgetReportedByHost() =
        with(kosmos) {
+288 −18

File changed.

Preview size limit exceeded, changes collapsed.

+14 −14
Original line number Diff line number Diff line
@@ -17,10 +17,9 @@
package com.android.systemui.communal.data.repository

import android.app.backup.BackupManager
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.os.UserHandle
import androidx.annotation.WorkerThread
import com.android.systemui.communal.data.backup.CommunalBackupUtils
import com.android.systemui.communal.data.db.CommunalItemRank
import com.android.systemui.communal.data.db.CommunalWidgetDao
@@ -36,15 +35,13 @@ 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.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

/** Encapsulates the state of widgets for communal mode. */
@@ -88,7 +85,6 @@ interface CommunalWidgetRepository {
class CommunalWidgetRepositoryImpl
@Inject
constructor(
    appWidgetManagerOptional: Optional<AppWidgetManager>,
    private val appWidgetHost: CommunalAppWidgetHost,
    @Background private val bgScope: CoroutineScope,
    @Background private val bgDispatcher: CoroutineDispatcher,
@@ -104,12 +100,13 @@ constructor(

    private val logger = Logger(logBuffer, TAG)

    private val appWidgetManager by appWidgetManagerOptional

    override val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
        communalWidgetDao
            .getWidgets()
            .map { it.mapNotNull(::mapToContentModel) }
        combine(
                communalWidgetDao.getWidgets(),
                communalWidgetHost.appWidgetProviders,
            ) { entries, providers ->
                entries.mapNotNull { entry -> mapToContentModel(entry, providers) }
            }
            // As this reads from a database and triggers IPCs to AppWidgetManager,
            // it should be executed in the background.
            .flowOn(bgDispatcher)
@@ -245,6 +242,9 @@ constructor(
                }
                appWidgetHost.deleteAppWidgetId(widgetId)
            }

            // Providers may have changed
            communalWidgetHost.refreshProviders()
        }
    }

@@ -255,12 +255,12 @@ constructor(
        }
    }

    @WorkerThread
    private fun mapToContentModel(
        entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>
        entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>,
        providers: Map<Int, AppWidgetProviderInfo?>,
    ): CommunalWidgetContentModel? {
        val (_, widgetId) = entry.value
        val providerInfo = appWidgetManager?.getAppWidgetInfo(widgetId) ?: return null
        val providerInfo = providers[widgetId] ?: return null
        return CommunalWidgetContentModel(
            appWidgetId = widgetId,
            providerInfo = providerInfo,
Loading