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

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

Merge changes If03395e1,I9da68853,I5a647719 into main

* changes:
  Placeholder for widgets pending install
  Introduce PackageInstallerMonitor
  Handle provider info updates
parents 288461f6 cfd183e2
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,
+15 −13
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ class PackageChangeRepositoryTest : SysuiTestCase() {
    @Mock private lateinit var context: Context
    @Mock private lateinit var packageManager: PackageManager
    @Mock private lateinit var handler: Handler
    @Mock private lateinit var packageInstallerMonitor: PackageInstallerMonitor

    private lateinit var repository: PackageChangeRepository
    private lateinit var updateMonitor: PackageUpdateMonitor
@@ -60,7 +61,8 @@ class PackageChangeRepositoryTest : SysuiTestCase() {
            MockitoAnnotations.initMocks(this@PackageChangeRepositoryTest)
            whenever(context.packageManager).thenReturn(packageManager)

            repository = PackageChangeRepositoryImpl { user ->
            repository =
                PackageChangeRepositoryImpl(packageInstallerMonitor) { user ->
                    updateMonitor =
                        PackageUpdateMonitor(
                            user = user,
+228 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.common.data.repository

import android.content.pm.PackageInstaller
import android.content.pm.PackageInstaller.SessionInfo
import android.graphics.Bitmap
import android.os.fakeExecutorHandler
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.PackageInstallSession
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.any
import com.android.systemui.util.mockito.eq
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.Correspondence
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class PackageInstallerMonitorTest : SysuiTestCase() {
    @Mock private lateinit var packageInstaller: PackageInstaller
    @Mock private lateinit var icon1: Bitmap
    @Mock private lateinit var icon2: Bitmap
    @Mock private lateinit var icon3: Bitmap

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

    private val handler = kosmos.fakeExecutorHandler

    private lateinit var session1: SessionInfo
    private lateinit var session2: SessionInfo
    private lateinit var session3: SessionInfo

    private lateinit var defaultSessions: List<SessionInfo>

    private lateinit var underTest: PackageInstallerMonitor

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)

        session1 =
            SessionInfo().apply {
                sessionId = 1
                appPackageName = "pkg_name_1"
                appIcon = icon1
            }
        session2 =
            SessionInfo().apply {
                sessionId = 2
                appPackageName = "pkg_name_2"
                appIcon = icon2
            }
        session3 =
            SessionInfo().apply {
                sessionId = 3
                appPackageName = "pkg_name_3"
                appIcon = icon3
            }
        defaultSessions = listOf(session1, session2)

        whenever(packageInstaller.allSessions).thenReturn(defaultSessions)
        whenever(packageInstaller.getSessionInfo(1)).thenReturn(session1)
        whenever(packageInstaller.getSessionInfo(2)).thenReturn(session2)

        underTest =
            PackageInstallerMonitor(
                handler,
                kosmos.applicationCoroutineScope,
                logcatLogBuffer("PackageInstallerRepositoryImplTest"),
                packageInstaller,
            )
    }

    @Test
    fun installSessions_callbacksRegisteredOnlyWhenFlowIsCollected() =
        testScope.runTest {
            // Verify callback not added before flow is collected
            verify(packageInstaller, never()).registerSessionCallback(any(), eq(handler))

            // Start collecting the flow
            val job =
                backgroundScope.launch {
                    underTest.installSessionsForPrimaryUser.collect {
                        // Do nothing with the value
                    }
                }
            runCurrent()

            // Verify callback added only after flow is collected
            val callback =
                withArgCaptor<PackageInstaller.SessionCallback> {
                    verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
                }

            // Verify callback not removed
            verify(packageInstaller, never()).unregisterSessionCallback(any())

            // Stop collecting the flow
            job.cancel()
            runCurrent()

            // Verify callback removed once flow stops being collected
            verify(packageInstaller).unregisterSessionCallback(eq(callback))
        }

    @Test
    fun installSessions_newSessionsAreAdded() =
        testScope.runTest {
            val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
            assertThat(installSessions)
                .comparingElementsUsing(represents)
                .containsExactlyElementsIn(defaultSessions)

            val callback =
                withArgCaptor<PackageInstaller.SessionCallback> {
                    verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
                }

            // New session added
            whenever(packageInstaller.getSessionInfo(3)).thenReturn(session3)
            callback.onCreated(3)
            runCurrent()

            // Verify flow updated with the new session
            assertThat(installSessions)
                .comparingElementsUsing(represents)
                .containsExactlyElementsIn(defaultSessions + session3)
        }

    @Test
    fun installSessions_finishedSessionsAreRemoved() =
        testScope.runTest {
            val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
            assertThat(installSessions)
                .comparingElementsUsing(represents)
                .containsExactlyElementsIn(defaultSessions)

            val callback =
                withArgCaptor<PackageInstaller.SessionCallback> {
                    verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
                }

            // Session 1 finished successfully
            callback.onFinished(1, /* success = */ true)
            runCurrent()

            // Verify flow updated with session 1 removed
            assertThat(installSessions)
                .comparingElementsUsing(represents)
                .containsExactlyElementsIn(defaultSessions - session1)
        }

    @Test
    fun installSessions_sessionsUpdatedOnBadgingChange() =
        testScope.runTest {
            val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
            assertThat(installSessions)
                .comparingElementsUsing(represents)
                .containsExactlyElementsIn(defaultSessions)

            val callback =
                withArgCaptor<PackageInstaller.SessionCallback> {
                    verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
                }

            // App icon for session 1 updated
            val newSession =
                SessionInfo().apply {
                    sessionId = 1
                    appPackageName = "pkg_name_1"
                    appIcon = mock()
                }
            whenever(packageInstaller.getSessionInfo(1)).thenReturn(newSession)
            callback.onBadgingChanged(1)
            runCurrent()

            // Verify flow updated with the new session 1
            assertThat(installSessions)
                .comparingElementsUsing(represents)
                .containsExactlyElementsIn(defaultSessions - session1 + newSession)
        }

    private val represents =
        Correspondence.from<PackageInstallSession, SessionInfo>(
            { actual, expected ->
                actual?.sessionId == expected?.sessionId &&
                    actual?.packageName == expected?.appPackageName &&
                    actual?.icon == expected?.getAppIcon()
            },
            "represents",
        )
}
+205 −14
Original line number Diff line number Diff line
@@ -17,16 +17,18 @@
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
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
@@ -46,10 +48,10 @@ 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
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
@@ -58,7 +60,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 +69,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,9 +80,11 @@ 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
    private val packageChangeRepository = kosmos.fakePackageChangeRepository

    private val fakeAllowlist =
        listOf(
@@ -96,6 +99,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 +107,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,
@@ -117,6 +120,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
                logBuffer,
                backupManager,
                backupUtils,
                packageChangeRepository,
            )
    }

@@ -126,15 +130,13 @@ 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()
            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = communalWidgetItemEntry.widgetId,
                        providerInfo = providerInfoA,
                        priority = communalItemRankEntry.rank,
@@ -145,6 +147,102 @@ 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.Available(
                        appWidgetId = 1,
                        providerInfo = providerInfoA,
                        priority = 1,
                    ),
                    CommunalWidgetContentModel.Available(
                        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).isNotNull()
            assertThat(communalWidgets)
                .containsExactly(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        providerInfo = providerInfoA,
                        priority = 1,
                    ),
                    CommunalWidgetContentModel.Available(
                        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.Available(
                        appWidgetId = 1,
                        // Verify that provider info updated
                        providerInfo = providerInfoC,
                        priority = 1,
                    ),
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 2,
                        providerInfo = providerInfoB,
                        priority = 2,
                    ),
                )
        }

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

    private fun installedProviders(providers: List<AppWidgetProviderInfo>) {
        whenever(appWidgetManager.installedProviders).thenReturn(providers)
    @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>) {
+41 −15

File changed.

Preview size limit exceeded, changes collapsed.

Loading