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

Commit 59b6c3b3 authored by Darrell Shi's avatar Darrell Shi
Browse files

Introduce CommunalWidgetRepositoryRemoteImpl

This change adds an implementation of `CommunalWidgetRepository` for the
headless system user. It routes requests and gets widget updates via IPC
from the foreground user.

Bug: 357621815
Test: atest GlanceableHubWidgetManagerServiceTest
Test: tested on device
Flag: com.android.systemui.secondary_user_widget_host

Change-Id: Ica63406e58b69bf3d4fba783ca700cca7d4e48f1
parent b761dbb9
Loading
Loading
Loading
Loading
+319 −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.communal.widgets

import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.content.Intent
import android.os.UserHandle
import android.testing.TestableLooper
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper
import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IGlanceableHubWidgetsListener
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
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.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidJUnit4::class)
class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private val appWidgetHostListenerCaptor = argumentCaptor<AppWidgetHost.AppWidgetHostListener>()

    private val widgetRepository = kosmos.fakeCommunalWidgetRepository
    private val appWidgetHost = mock<CommunalAppWidgetHost>()
    private val communalWidgetHost = mock<CommunalWidgetHost>()
    private val multiUserHelper = kosmos.fakeGlanceableHubMultiUserHelper

    private lateinit var underTest: GlanceableHubWidgetManagerService

    @Before
    fun setup() {
        underTest =
            GlanceableHubWidgetManagerService(
                widgetRepository,
                appWidgetHost,
                communalWidgetHost,
                multiUserHelper,
                logcatLogBuffer("GlanceableHubWidgetManagerServiceTest"),
            )
    }

    @Test
    fun appWidgetHost_listenWhenServiceIsBound() {
        underTest.onCreate()
        verify(appWidgetHost).startListening()
        verify(communalWidgetHost).startObservingHost()
        verify(appWidgetHost, never()).stopListening()
        verify(communalWidgetHost, never()).stopObservingHost()

        underTest.onDestroy()
        verify(appWidgetHost).stopListening()
        verify(communalWidgetHost).stopObservingHost()
    }

    @Test
    fun widgetsListener_getWidgetUpdates() =
        testScope.runTest {
            setupWidgets()

            // Bind service
            val binder = underTest.onBind(Intent())
            val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder)

            // Verify the update is as expected
            val widgets by collectLastValue(service.listenForWidgetUpdates())
            assertThat(widgets).hasSize(3)
            assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue()
            assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue()
        }

    @Test
    fun widgetsListener_multipleListeners_eachGetsWidgetUpdates() =
        testScope.runTest {
            setupWidgets()

            // Bind service
            val binder = underTest.onBind(Intent())
            val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder)

            // Verify the update for the first listener is as expected
            val widgets1 by collectLastValue(service.listenForWidgetUpdates())
            assertThat(widgets1).hasSize(3)
            assertThat(widgets1?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue()
            assertThat(widgets1?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets1?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue()

            // Verify the update for the second listener is as expected
            val widgets2 by collectLastValue(service.listenForWidgetUpdates())
            assertThat(widgets2).hasSize(3)
            assertThat(widgets2?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue()
            assertThat(widgets2?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets2?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue()
        }

    @Test
    fun setAppWidgetHostListener_getUpdates() =
        testScope.runTest {
            // Bind service
            val binder = underTest.onBind(Intent())
            val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder)

            // Set listener
            val listener = mock<IGlanceableHubWidgetManagerService.IAppWidgetHostListener>()
            service.setAppWidgetHostListener(1, listener)

            // Verify a listener is set on the host
            verify(appWidgetHost).setListener(eq(1), appWidgetHostListenerCaptor.capture())
            val appWidgetHostListener = appWidgetHostListenerCaptor.firstValue

            // Each update should be passed to the listener
            val providerInfo = mock<AppWidgetProviderInfo>()
            appWidgetHostListener.onUpdateProviderInfo(providerInfo)
            verify(listener).onUpdateProviderInfo(providerInfo)

            val remoteViews = mock<RemoteViews>()
            appWidgetHostListener.updateAppWidget(remoteViews)
            verify(listener).updateAppWidget(remoteViews)

            appWidgetHostListener.updateAppWidgetDeferred("pkg", 1)
            verify(listener).updateAppWidgetDeferred("pkg", 1)

            appWidgetHostListener.onViewDataChanged(1)
            verify(listener).onViewDataChanged(1)
        }

    @Test
    fun addWidget_getWidgetUpdate() =
        testScope.runTest {
            setupWidgets()

            // Bind service
            val binder = underTest.onBind(Intent())
            val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder)

            // Verify the update is as expected
            val widgets by collectLastValue(service.listenForWidgetUpdates())
            assertThat(widgets).hasSize(3)
            assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue()
            assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue()

            // Add a widget
            service.addWidget(ComponentName("pkg_4", "cls_4"), UserHandle.of(0), 3)
            runCurrent()

            // Verify an update pushed with widget 4 added
            assertThat(widgets).hasSize(4)
            assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue()
            assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue()
            assertThat(widgets?.get(3)?.has(4, "pkg_4/cls_4", 3, 3)).isTrue()
        }

    @Test
    fun deleteWidget_getWidgetUpdate() =
        testScope.runTest {
            setupWidgets()

            // Bind service
            val binder = underTest.onBind(Intent())
            val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder)

            // Verify the update is as expected
            val widgets by collectLastValue(service.listenForWidgetUpdates())
            assertThat(widgets).hasSize(3)
            assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue()
            assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue()

            // Delete a widget
            service.deleteWidget(1)
            runCurrent()

            // Verify an update pushed with widget 1 removed
            assertThat(widgets).hasSize(2)
            assertThat(widgets?.get(0)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets?.get(1)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue()
        }

    @Test
    fun updateWidgetOrder_getWidgetUpdate() =
        testScope.runTest {
            setupWidgets()

            // Bind service
            val binder = underTest.onBind(Intent())
            val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder)

            // Verify the update is as expected
            val widgets by collectLastValue(service.listenForWidgetUpdates())
            assertThat(widgets).hasSize(3)
            assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue()
            assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue()

            // Update widget order
            service.updateWidgetOrder(intArrayOf(1, 2, 3), intArrayOf(2, 1, 0))
            runCurrent()

            // Verify an update pushed with the new order
            assertThat(widgets).hasSize(3)
            assertThat(widgets?.get(0)?.has(3, "pkg_3/cls_3", 0, 6)).isTrue()
            assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets?.get(2)?.has(1, "pkg_1/cls_1", 2, 3)).isTrue()
        }

    @Test
    fun resizeWidget_getWidgetUpdate() =
        testScope.runTest {
            setupWidgets()

            // Bind service
            val binder = underTest.onBind(Intent())
            val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder)

            // Verify the update is as expected
            val widgets by collectLastValue(service.listenForWidgetUpdates())
            assertThat(widgets).hasSize(3)
            assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue()
            assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue()

            // Resize widget 1 from spanY 3 to 6
            service.resizeWidget(1, 6, intArrayOf(1, 2, 3), intArrayOf(0, 1, 2))
            runCurrent()

            // Verify an update pushed with the new size for widget 1
            assertThat(widgets).hasSize(3)
            assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 6)).isTrue()
            assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue()
            assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue()
        }

    private fun setupWidgets() {
        widgetRepository.addWidget(
            appWidgetId = 1,
            componentName = "pkg_1/cls_1",
            rank = 0,
            spanY = 3,
        )
        widgetRepository.addWidget(
            appWidgetId = 2,
            componentName = "pkg_2/cls_2",
            rank = 1,
            spanY = 3,
        )
        widgetRepository.addWidget(
            appWidgetId = 3,
            componentName = "pkg_3/cls_3",
            rank = 2,
            spanY = 6,
        )
    }

    private fun IGlanceableHubWidgetManagerService.listenForWidgetUpdates() =
        conflatedCallbackFlow<List<CommunalWidgetContentModel>> {
            val listener =
                object : IGlanceableHubWidgetsListener.Stub() {
                    override fun onWidgetsUpdated(widgets: List<CommunalWidgetContentModel>) {
                        trySend(widgets)
                    }
                }
            addWidgetsListener(listener)
            awaitClose { removeWidgetsListener(listener) }
        }

    private fun CommunalWidgetContentModel.has(
        appWidgetId: Int,
        componentName: String,
        rank: Int,
        spanY: Int,
    ): Boolean {
        return this is CommunalWidgetContentModel.Available &&
            this.appWidgetId == appWidgetId &&
            this.providerInfo.provider.flattenToString() == componentName &&
            this.rank == rank &&
            this.spanY == spanY
    }
}
+4 −4
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import androidx.room.RoomDatabase
import androidx.room.Transaction
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteDatabase
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.communal.nano.CommunalHubState
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.widgets.CommunalWidgetHost
@@ -39,7 +40,6 @@ import javax.inject.Named
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import com.android.app.tracing.coroutines.launchTraced as launch

/**
 * Callback that will be invoked when the Room database is created. Then the database will be
@@ -224,9 +224,9 @@ interface CommunalWidgetDao {
    ): Long {
        val widgets = getWidgetsNow()

        // If rank is not specified, rank it last by finding the current maximum rank and increment
        // by 1. If the new widget is the first widget, set the rank to 0.
        val newRank = rank ?: widgets.keys.maxOfOrNull { it.rank + 1 } ?: 0
        // If rank is not specified (null or less than 0), rank it last by finding the current
        // maximum rank and increment by 1. If the new widget is the first widget, set rank to 0.
        val newRank = rank?.takeIf { it >= 0 } ?: widgets.keys.maxOfOrNull { it.rank + 1 } ?: 0

        // Shift widgets after [rank], unless widget is added at the end.
        if (rank != null) {
+1 −1
Original line number Diff line number Diff line
@@ -55,7 +55,7 @@ import kotlinx.coroutines.flow.map

/** Encapsulates the state of widgets for communal mode. */
interface CommunalWidgetRepository {
    /** A flow of information about active communal widgets stored in database. */
    /** A flow of the list of Glanceable Hub widgets ordered by rank. */
    val communalWidgets: Flow<List<CommunalWidgetContentModel>>

    /**
+16 −3
Original line number Diff line number Diff line
@@ -17,11 +17,24 @@

package com.android.systemui.communal.data.repository

import dagger.Binds
import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper
import dagger.Lazy
import dagger.Module
import dagger.Provides

@Module
interface CommunalWidgetRepositoryModule {
    @Binds
    fun communalWidgetRepository(impl: CommunalWidgetRepositoryLocalImpl): CommunalWidgetRepository
    companion object {
        @Provides
        fun provideCommunalWidgetRepository(
            localImpl: Lazy<CommunalWidgetRepositoryLocalImpl>,
            remoteImpl: Lazy<CommunalWidgetRepositoryRemoteImpl>,
            helper: GlanceableHubMultiUserHelper,
        ): CommunalWidgetRepository {
            // Provide an implementation based on the current user.
            return if (helper.glanceableHubHsumFlagEnabled && helper.isInHeadlessSystemUser())
                remoteImpl.get()
            else localImpl.get()
        }
    }
}
+85 −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.communal.data.repository

import android.content.ComponentName
import android.os.UserHandle
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper
import com.android.systemui.communal.widgets.GlanceableHubWidgetManager
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch

/**
 * The remote implementation of the [CommunalWidgetRepository] that should be injected in a headless
 * system user process. This implementation receives widget data from and routes requests to the
 * remote service in the foreground user.
 */
@SysUISingleton
class CommunalWidgetRepositoryRemoteImpl
@Inject
constructor(
    @Background private val bgScope: CoroutineScope,
    private val glanceableHubWidgetManager: GlanceableHubWidgetManager,
    glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper,
) : CommunalWidgetRepository {

    init {
        // This is the implementation for the headless system user. For the foreground user
        // implementation see [CommunalWidgetRepositoryLocalImpl].
        glanceableHubMultiUserHelper.assertInHeadlessSystemUser()
    }

    override val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
        glanceableHubWidgetManager.widgets

    override fun addWidget(
        provider: ComponentName,
        user: UserHandle,
        rank: Int?,
        configurator: WidgetConfigurator?,
    ) {
        bgScope.launch { glanceableHubWidgetManager.addWidget(provider, user, rank, configurator) }
    }

    override fun deleteWidget(widgetId: Int) {
        bgScope.launch { glanceableHubWidgetManager.deleteWidget(widgetId) }
    }

    override fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) {
        bgScope.launch { glanceableHubWidgetManager.updateWidgetOrder(widgetIdToRankMap) }
    }

    override fun resizeWidget(appWidgetId: Int, spanY: Int, widgetIdToRankMap: Map<Int, Int>) {
        bgScope.launch {
            glanceableHubWidgetManager.resizeWidget(appWidgetId, spanY, widgetIdToRankMap)
        }
    }

    override fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) {
        throw IllegalStateException("Restore widgets should be performed on a foreground user")
    }

    override fun abortRestoreWidgets() {
        throw IllegalStateException("Restore widgets should be performed on a foreground user")
    }
}
Loading