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

Commit cfd183e2 authored by Darrell Shi's avatar Darrell Shi
Browse files

Placeholder for widgets pending install

This change adds a new subtype for communal widget content: pending.
This represents widgets that have been added in the hub but are being
installed. Now after a backup is restored, restored widgets whose
package is pending install will show up in the hub as a placeholder. The
UX is not final and may need polish.

Test: atest CommunalWidgetRepositoryImplTest
Test: atest CommmunalAppWidgetHostStartableTest
Test: atest CommunalInteractorTest
Test: manually with instructions at go/glanceable-hub-br
Bug: 330945453
Fix: 330945453
Flag: ACONFIG com.android.systemui.communal_hub TEAMFOOD
Change-Id: If03395e1ca22e2c93ab0ac41a240bbae301c1409
parent 1f323b51
Loading
Loading
Loading
Loading
+59 −27
Original line number Diff line number Diff line
@@ -804,6 +804,8 @@ private fun CommunalContent(
        is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier)
        is CommunalContentModel.WidgetContent.DisabledWidget ->
            DisabledWidgetPlaceholder(model, viewModel, modifier)
        is CommunalContentModel.WidgetContent.PendingWidget ->
            PendingWidgetPlaceholder(model, modifier)
        is CommunalContentModel.CtaTileInViewMode -> CtaTileInViewModeContent(viewModel, modifier)
        is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
        is CommunalContentModel.Tutorial -> TutorialContent(modifier)
@@ -1074,12 +1076,42 @@ fun DisabledWidgetPlaceholder(
        Image(
            painter = rememberDrawablePainter(icon.loadDrawable(context)),
            contentDescription = stringResource(R.string.icon_description_for_disabled_widget),
            modifier = Modifier.size(48.dp),
            modifier = Modifier.size(Dimensions.IconSize),
            colorFilter = ColorFilter.colorMatrix(Colors.DisabledColorFilter),
        )
    }
}

@Composable
fun PendingWidgetPlaceholder(
    model: CommunalContentModel.WidgetContent.PendingWidget,
    modifier: Modifier = Modifier,
) {
    val context = LocalContext.current
    val icon: Icon =
        if (model.icon != null) {
            Icon.createWithBitmap(model.icon)
        } else {
            Icon.createWithResource(context, android.R.drawable.sym_def_app_icon)
        }

    Column(
        modifier =
            modifier.background(
                MaterialTheme.colorScheme.surfaceVariant,
                RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
            ),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Image(
            painter = rememberDrawablePainter(icon.loadDrawable(context)),
            contentDescription = stringResource(R.string.icon_description_for_pending_widget),
            modifier = Modifier.size(Dimensions.IconSize),
        )
    }
}

@Composable
private fun SmartspaceContent(
    model: CommunalContentModel.Smartspace,
+111 −7
Original line number Diff line number Diff line
@@ -22,10 +22,13 @@ import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTI
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
import android.content.ComponentName
import android.content.applicationContext
import android.graphics.Bitmap
import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.data.repository.fakePackageChangeRepository
import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.communal.data.backup.CommunalBackupUtils
import com.android.systemui.communal.data.db.CommunalItemRank
import com.android.systemui.communal.data.db.CommunalWidgetDao
@@ -45,6 +48,7 @@ import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
@@ -80,6 +84,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val packageChangeRepository = kosmos.fakePackageChangeRepository

    private val fakeAllowlist =
        listOf(
@@ -115,6 +120,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
                logBuffer,
                backupManager,
                backupUtils,
                packageChangeRepository,
            )
    }

@@ -130,7 +136,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
            verify(communalWidgetDao).getWidgets()
            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = communalWidgetItemEntry.widgetId,
                        providerInfo = providerInfoA,
                        priority = communalItemRankEntry.rank,
@@ -166,12 +172,12 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
            val communalWidgets by collectLastValue(underTest.communalWidgets)
            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        providerInfo = providerInfoA,
                        priority = 1,
                    ),
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 2,
                        providerInfo = providerInfoB,
                        priority = 2,
@@ -198,14 +204,15 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {

            // Expect two widgets
            val communalWidgets by collectLastValue(underTest.communalWidgets)
            assertThat(communalWidgets).isNotNull()
            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        providerInfo = providerInfoA,
                        priority = 1,
                    ),
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 2,
                        providerInfo = providerInfoB,
                        priority = 2,
@@ -222,13 +229,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {

            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        // Verify that provider info updated
                        providerInfo = providerInfoC,
                        priority = 1,
                    ),
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 2,
                        providerInfo = providerInfoB,
                        priority = 2,
@@ -525,6 +532,103 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
            assertThat(restoredWidget2.rank).isEqualTo(expectedWidget2.rank)
        }

    @Test
    fun pendingWidgets() =
        testScope.runTest {
            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),
                )

            // Widget 1 is installed
            fakeProviders.value = mapOf(1 to providerInfoA)

            // Widget 2 is pending install
            val fakeIcon = mock<Bitmap>()
            packageChangeRepository.setInstallSessions(
                listOf(
                    PackageInstallSession(
                        sessionId = 1,
                        packageName = "pk_2",
                        icon = fakeIcon,
                        user = UserHandle.CURRENT,
                    )
                )
            )

            val communalWidgets by collectLastValue(underTest.communalWidgets)
            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        providerInfo = providerInfoA,
                        priority = 1,
                    ),
                    CommunalWidgetContentModel.Pending(
                        appWidgetId = 2,
                        priority = 2,
                        packageName = "pk_2",
                        icon = fakeIcon,
                        user = UserHandle.CURRENT,
                    ),
                )
        }

    @Test
    fun pendingWidgets_pendingWidgetBecomesAvailableAfterInstall() =
        testScope.runTest {
            fakeWidgets.value =
                mapOf(
                    CommunalItemRank(uid = 1L, rank = 1) to
                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
                )

            // Widget 1 is pending install
            val fakeIcon = mock<Bitmap>()
            packageChangeRepository.setInstallSessions(
                listOf(
                    PackageInstallSession(
                        sessionId = 1,
                        packageName = "pk_1",
                        icon = fakeIcon,
                        user = UserHandle.CURRENT,
                    )
                )
            )

            val communalWidgets by collectLastValue(underTest.communalWidgets)
            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel.Pending(
                        appWidgetId = 1,
                        priority = 1,
                        packageName = "pk_1",
                        icon = fakeIcon,
                        user = UserHandle.CURRENT,
                    ),
                )

            // Package for widget 1 finished installing
            packageChangeRepository.setInstallSessions(emptyList())

            // Provider info for widget 1 becomes available
            fakeProviders.value = mapOf(1 to providerInfoA)

            runCurrent()

            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        providerInfo = providerInfoA,
                        priority = 1,
                    ),
                )
        }

    private fun setAppWidgetIds(ids: List<Int>) {
        whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray())
    }
+41 −15
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.app.smartspace.SmartspaceTarget
import android.appwidget.AppWidgetProviderInfo
import android.content.Intent
import android.content.pm.UserInfo
import android.graphics.Bitmap
import android.os.UserHandle
import android.os.UserManager
import android.os.userManager
@@ -871,7 +872,14 @@ class CommunalInteractorTest : SysuiTestCase() {
            // One widget is filtered out and the remaining two link to main user id.
            assertThat(checkNotNull(widgetContent).size).isEqualTo(2)
            widgetContent!!.forEachIndexed { _, model ->
                assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id)
                assertThat(model is CommunalContentModel.WidgetContent.Widget).isTrue()
                assertThat(
                        (model as CommunalContentModel.WidgetContent.Widget)
                            .providerInfo
                            .profile
                            ?.identifier
                    )
                    .isEqualTo(MAIN_USER_INFO.id)
            }
        }

@@ -1037,9 +1045,9 @@ class CommunalInteractorTest : SysuiTestCase() {
            runCurrent()

            val widgetContent by collectLastValue(underTest.widgetContent)
            // Given three widgets, and one of them is associated with work profile.
            // One available work widget, one pending work widget, and one regular available widget.
            val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
            val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
            val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id)
            val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
            val widgets = listOf(widget1, widget2, widget3)
            widgetRepository.setCommunalWidgets(widgets)
@@ -1049,11 +1057,9 @@ class CommunalInteractorTest : SysuiTestCase() {
                DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL
            )

            // Widget under work profile is filtered out and the remaining two link to main user id.
            assertThat(widgetContent).hasSize(2)
            widgetContent!!.forEach { model ->
                assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id)
            }
            // Widgets under work profile are filtered out. Only the regular widget remains.
            assertThat(widgetContent).hasSize(1)
            assertThat(widgetContent?.get(0)?.appWidgetId).isEqualTo(3)
        }

    @Test
@@ -1076,7 +1082,7 @@ class CommunalInteractorTest : SysuiTestCase() {
            val widgetContent by collectLastValue(underTest.widgetContent)
            // Given three widgets, and one of them is associated with work profile.
            val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
            val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
            val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id)
            val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
            val widgets = listOf(widget1, widget2, widget3)
            widgetRepository.setCommunalWidgets(widgets)
@@ -1086,10 +1092,11 @@ class CommunalInteractorTest : SysuiTestCase() {
                DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE
            )

            // Widget under work profile is available.
            // Widgets under work profile are available.
            assertThat(widgetContent).hasSize(3)
            assertThat(widgetContent!![0].providerInfo.profile?.identifier)
                .isEqualTo(USER_INFO_WORK.id)
            assertThat(widgetContent?.get(0)?.appWidgetId).isEqualTo(1)
            assertThat(widgetContent?.get(1)?.appWidgetId).isEqualTo(2)
            assertThat(widgetContent?.get(2)?.appWidgetId).isEqualTo(3)
        }

    @Test
@@ -1182,8 +1189,11 @@ class CommunalInteractorTest : SysuiTestCase() {
        )
    }

    private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
        mock<CommunalWidgetContentModel> {
    private fun createWidgetForUser(
        appWidgetId: Int,
        userId: Int
    ): CommunalWidgetContentModel.Available =
        mock<CommunalWidgetContentModel.Available> {
            whenever(this.appWidgetId).thenReturn(appWidgetId)
            val providerInfo =
                mock<AppWidgetProviderInfo>().apply {
@@ -1193,11 +1203,27 @@ class CommunalInteractorTest : SysuiTestCase() {
            whenever(this.providerInfo).thenReturn(providerInfo)
        }

    private fun createPendingWidgetForUser(
        appWidgetId: Int,
        priority: Int = 0,
        packageName: String = "",
        icon: Bitmap? = null,
        userId: Int = 0,
    ): CommunalWidgetContentModel.Pending {
        return CommunalWidgetContentModel.Pending(
            appWidgetId = appWidgetId,
            priority = priority,
            packageName = packageName,
            icon = icon,
            user = UserHandle(userId),
        )
    }

    private fun createWidgetWithCategory(
        appWidgetId: Int,
        category: Int
    ): CommunalWidgetContentModel =
        mock<CommunalWidgetContentModel> {
        mock<CommunalWidgetContentModel.Available> {
            whenever(this.appWidgetId).thenReturn(appWidgetId)
            val providerInfo = mock<AppWidgetProviderInfo>().apply { widgetCategory = category }
            whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))
+4 −4
Original line number Diff line number Diff line
@@ -123,12 +123,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
            // Widgets available.
            val widgets =
                listOf(
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 0,
                        priority = 30,
                        providerInfo = providerInfo,
                    ),
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        priority = 20,
                        providerInfo = providerInfo,
@@ -177,12 +177,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
            // Widgets available.
            val widgets =
                listOf(
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 0,
                        priority = 30,
                        providerInfo = providerInfo,
                    ),
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        priority = 20,
                        providerInfo = providerInfo,
+3 −3
Original line number Diff line number Diff line
@@ -186,12 +186,12 @@ class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
            // Widgets available.
            val widgets =
                listOf(
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 0,
                        priority = 30,
                        providerInfo = providerInfo,
                    ),
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        priority = 20,
                        providerInfo = providerInfo,
@@ -245,7 +245,7 @@ class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {

            widgetRepository.setCommunalWidgets(
                listOf(
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        priority = 1,
                        providerInfo = providerInfo,
Loading