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

Commit 435eff7f authored by Coco Duan's avatar Coco Duan
Browse files

Remove associated widgets after work profile is removed

- Filter communalInteractor.widgetContent by profile id to ensure
  always returning valid widgets associated with existing users.
- In CommunalAppWidgetHostStartable, when communal is showing, delete
  any orphaned widget stored in db associated with deleted user profile.

Bug: b/324465218
Test: remove work profile and verify work widgets removed from GH
Test: atest CommunalAppWidgetHostCoreStartableTest
Test: atest CommunalInteractorTest
Flag: ACONFIG com.android.systemui.communal_hub STAGING
Change-Id: Ifbc0e9e384103a3cb5062860d633965297d308e5
parent ad18e2f3
Loading
Loading
Loading
Loading
+64 −19
Original line number Diff line number Diff line
@@ -18,7 +18,9 @@
package com.android.systemui.communal.domain.interactor

import android.app.smartspace.SmartspaceTarget
import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
import android.os.UserHandle
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -51,6 +53,8 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
@@ -96,6 +100,7 @@ class CommunalInteractorTest : SysuiTestCase() {
    private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository
    private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter
    private lateinit var sceneInteractor: SceneInteractor
    private lateinit var userTracker: FakeUserTracker

    private lateinit var underTest: CommunalInteractor

@@ -113,6 +118,7 @@ class CommunalInteractorTest : SysuiTestCase() {
        editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter
        communalPrefsRepository = kosmos.fakeCommunalPrefsRepository
        sceneInteractor = kosmos.sceneInteractor
        userTracker = kosmos.fakeUserTracker

        whenever(mainUser.isMain).thenReturn(true)
        whenever(secondaryUser.isMain).thenReturn(false)
@@ -207,25 +213,19 @@ class CommunalInteractorTest : SysuiTestCase() {
            keyguardRepository.setKeyguardOccluded(false)
            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)

            // Widgets are available.
            val widgets =
                listOf(
                    CommunalWidgetContentModel(
                        appWidgetId = 0,
                        priority = 30,
                        providerInfo = mock(),
                    ),
                    CommunalWidgetContentModel(
                        appWidgetId = 1,
                        priority = 20,
                        providerInfo = mock(),
                    ),
                    CommunalWidgetContentModel(
                        appWidgetId = 2,
                        priority = 10,
                        providerInfo = mock(),
                    ),
            val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK)
            userRepository.setUserInfos(userInfos)
            userTracker.set(
                userInfos = userInfos,
                selectedUserIndex = 0,
            )
            runCurrent()

            // Widgets available.
            val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
            val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
            val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
            val widgets = listOf(widget1, widget2, widget3)
            widgetRepository.setCommunalWidgets(widgets)

            val widgetContent by collectLastValue(underTest.widgetContent)
@@ -752,6 +752,38 @@ class CommunalInteractorTest : SysuiTestCase() {
            verify(editWidgetsActivityStarter).startActivity(widgetKey)
        }

    @Test
    fun filterWidgets_whenUserProfileRemoved() =
        testScope.runTest {
            // Keyguard showing, and tutorial completed.
            keyguardRepository.setKeyguardShowing(true)
            keyguardRepository.setKeyguardOccluded(false)
            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)

            // Only main user exists.
            val userInfos = listOf(MAIN_USER_INFO)
            userRepository.setUserInfos(userInfos)
            userTracker.set(
                userInfos = userInfos,
                selectedUserIndex = 0,
            )
            runCurrent()

            val widgetContent by collectLastValue(underTest.widgetContent)
            // Given three widgets, and one of them is associated with pre-existing work profile.
            val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
            val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
            val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
            val widgets = listOf(widget1, widget2, widget3)
            widgetRepository.setCommunalWidgets(widgets)

            // 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)
            }
        }

    private fun smartspaceTimer(id: String, timestamp: Long = 0L): SmartspaceTarget {
        val timer = mock(SmartspaceTarget::class.java)
        whenever(timer.smartspaceTargetId).thenReturn(id)
@@ -760,4 +792,17 @@ class CommunalInteractorTest : SysuiTestCase() {
        whenever(timer.creationTimeMillis).thenReturn(timestamp)
        return timer
    }

    private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
        mock<CommunalWidgetContentModel> {
            whenever(this.appWidgetId).thenReturn(appWidgetId)
            val providerInfo = mock<AppWidgetProviderInfo>()
            whenever(providerInfo.profile).thenReturn(UserHandle(userId))
            whenever(this.providerInfo).thenReturn(providerInfo)
        }

    private companion object {
        val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
        val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
    }
}
+18 −4
Original line number Diff line number Diff line
@@ -17,6 +17,9 @@
package com.android.systemui.communal.view.viewmodel

import android.app.smartspace.SmartspaceTarget
import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
import android.os.UserHandle
import android.provider.Settings
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -39,6 +42,7 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
@@ -59,6 +63,7 @@ import org.mockito.MockitoAnnotations
class CommunalEditModeViewModelTest : SysuiTestCase() {
    @Mock private lateinit var mediaHost: MediaHost
    @Mock private lateinit var uiEventLogger: UiEventLogger
    @Mock private lateinit var providerInfo: AppWidgetProviderInfo

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
@@ -78,6 +83,11 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
        widgetRepository = kosmos.fakeCommunalWidgetRepository
        smartspaceRepository = kosmos.fakeSmartspaceRepository
        mediaRepository = kosmos.fakeCommunalMediaRepository
        kosmos.fakeUserTracker.set(
            userInfos = listOf(MAIN_USER_INFO),
            selectedUserIndex = 0,
        )
        whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))

        underTest =
            CommunalEditModeViewModel(
@@ -100,12 +110,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
                    CommunalWidgetContentModel(
                        appWidgetId = 0,
                        priority = 30,
                        providerInfo = mock(),
                        providerInfo = providerInfo,
                    ),
                    CommunalWidgetContentModel(
                        appWidgetId = 1,
                        priority = 20,
                        providerInfo = mock(),
                        providerInfo = providerInfo,
                    ),
                )
            widgetRepository.setCommunalWidgets(widgets)
@@ -156,12 +166,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
                    CommunalWidgetContentModel(
                        appWidgetId = 0,
                        priority = 30,
                        providerInfo = mock(),
                        providerInfo = providerInfo,
                    ),
                    CommunalWidgetContentModel(
                        appWidgetId = 1,
                        priority = 20,
                        providerInfo = mock(),
                        providerInfo = providerInfo,
                    ),
                )
            widgetRepository.setCommunalWidgets(widgets)
@@ -205,4 +215,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
        underTest.onReorderWidgetCancel()
        verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
    }

    private companion object {
        val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
    }
}
+16 −3
Original line number Diff line number Diff line
@@ -17,7 +17,9 @@
package com.android.systemui.communal.view.viewmodel

import android.app.smartspace.SmartspaceTarget
import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
import android.os.UserHandle
import android.provider.Settings
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -45,13 +47,13 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.FakeUserRepository
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
@@ -71,6 +73,7 @@ import org.mockito.MockitoAnnotations
class CommunalViewModelTest : SysuiTestCase() {
    @Mock private lateinit var mediaHost: MediaHost
    @Mock private lateinit var user: UserInfo
    @Mock private lateinit var providerInfo: AppWidgetProviderInfo

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
@@ -98,6 +101,12 @@ class CommunalViewModelTest : SysuiTestCase() {
        kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
        mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)

        kosmos.fakeUserTracker.set(
            userInfos = listOf(MAIN_USER_INFO),
            selectedUserIndex = 0,
        )
        whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))

        underTest =
            CommunalViewModel(
                testScope,
@@ -147,12 +156,12 @@ class CommunalViewModelTest : SysuiTestCase() {
                    CommunalWidgetContentModel(
                        appWidgetId = 0,
                        priority = 30,
                        providerInfo = mock(),
                        providerInfo = providerInfo,
                    ),
                    CommunalWidgetContentModel(
                        appWidgetId = 1,
                        priority = 20,
                        providerInfo = mock(),
                        providerInfo = providerInfo,
                    ),
                )
            widgetRepository.setCommunalWidgets(widgets)
@@ -225,4 +234,8 @@ class CommunalViewModelTest : SysuiTestCase() {
        userRepository.setUserInfos(listOf(user))
        userRepository.setSelectedUserInfo(user)
    }

    private companion object {
        val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
    }
}
+54 −1
Original line number Diff line number Diff line
@@ -16,7 +16,9 @@

package com.android.systemui.communal.widgets

import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
@@ -32,6 +34,7 @@ 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.settings.fakeUserTracker
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.mock
@@ -65,7 +68,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
        kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO, USER_INFO_WORK))
        kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
        mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)

@@ -76,6 +79,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
            CommunalAppWidgetHostStartable(
                appWidgetHost,
                kosmos.communalInteractor,
                kosmos.fakeUserTracker,
                kosmos.applicationCoroutineScope,
                kosmos.testDispatcher,
            )
@@ -170,6 +174,46 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
            }
        }

    @Test
    fun removeWidgetsForDeletedProfile_whenCommunalIsAvailable() =
        with(kosmos) {
            testScope.runTest {
                // Communal is available and work profile is configured.
                setCommunalAvailable(true)
                kosmos.fakeUserTracker.set(
                    userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK),
                    selectedUserIndex = 0,
                )
                val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
                val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
                val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
                val widgets = listOf(widget1, widget2, widget3)
                fakeCommunalWidgetRepository.setCommunalWidgets(widgets)

                underTest.start()
                runCurrent()

                val communalWidgets by
                    collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
                assertThat(communalWidgets).containsExactly(widget1, widget2, widget3)

                // Unlock the device and remove work profile.
                fakeKeyguardRepository.setKeyguardShowing(false)
                kosmos.fakeUserTracker.set(
                    userInfos = listOf(MAIN_USER_INFO),
                    selectedUserIndex = 0,
                )
                runCurrent()

                // Communal becomes available.
                fakeKeyguardRepository.setKeyguardShowing(true)
                runCurrent()

                // Widget created for work profile is removed.
                assertThat(communalWidgets).containsExactly(widget2, widget3)
            }
        }

    private suspend fun setCommunalAvailable(available: Boolean) =
        with(kosmos) {
            fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
@@ -179,7 +223,16 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
            fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, settingsValue, MAIN_USER_INFO.id)
        }

    private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
        mock<CommunalWidgetContentModel> {
            whenever(this.appWidgetId).thenReturn(appWidgetId)
            val providerInfo = mock<AppWidgetProviderInfo>()
            whenever(providerInfo.profile).thenReturn(UserHandle(userId))
            whenever(this.providerInfo).thenReturn(providerInfo)
        }

    private companion object {
        val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
        val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
    }
}
+24 −1
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import com.android.systemui.communal.shared.model.CommunalContentSize.FULL
import com.android.systemui.communal.shared.model.CommunalContentSize.HALF
import com.android.systemui.communal.shared.model.CommunalContentSize.THIRD
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
@@ -45,6 +46,7 @@ import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.settings.UserTracker
import com.android.systemui.smartspace.data.repository.SmartspaceRepository
import com.android.systemui.util.kotlin.BooleanFlowOperators.and
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
@@ -59,6 +61,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
@@ -82,6 +85,7 @@ constructor(
    communalSettingsInteractor: CommunalSettingsInteractor,
    private val appWidgetHost: CommunalAppWidgetHost,
    private val editWidgetsActivityStarter: EditWidgetsActivityStarter,
    private val userTracker: UserTracker,
    sceneInteractor: SceneInteractor,
    sceneContainerFlags: SceneContainerFlags,
    @CommunalLog logBuffer: LogBuffer,
@@ -262,10 +266,16 @@ constructor(
    fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) =
        widgetRepository.updateWidgetOrder(widgetIdToPriorityMap)

    /** All widgets present in db. */
    val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
        isCommunalAvailable.flatMapLatest { available ->
            if (!available) emptyFlow() else widgetRepository.communalWidgets
        }

    /** A list of widget content to be displayed in the communal hub. */
    val widgetContent: Flow<List<CommunalContentModel.Widget>> =
        widgetRepository.communalWidgets.map { widgets ->
            widgets.map Widget@{ widget ->
            filterWidgetsByExistingUsers(widgets).map Widget@{ widget ->
                return@Widget CommunalContentModel.Widget(
                    appWidgetId = widget.appWidgetId,
                    providerInfo = widget.providerInfo,
@@ -345,6 +355,19 @@ constructor(
            return@combine ongoingContent
        }

    /**
     * Filter and retain widgets associated with an existing user, safeguarding against displaying
     * stale data following user deletion.
     */
    private fun filterWidgetsByExistingUsers(
        list: List<CommunalWidgetContentModel>,
    ): List<CommunalWidgetContentModel> {
        val currentUserIds = userTracker.userProfiles.map { it.id }.toSet()
        return list.filter { widget ->
            currentUserIds.contains(widget.providerInfo.profile?.identifier)
        }
    }

    companion object {
        /**
         * The user activity timeout which should be used when the communal hub is opened. A value
Loading