diff options
| author | 2023-10-19 19:00:44 +0000 | |
|---|---|---|
| committer | 2023-10-19 19:00:44 +0000 | |
| commit | 3bd9e93f540affa3e19009421d1f789f90e3fb09 (patch) | |
| tree | 37edaeb11a89cfad5c9a5f36e063bebc2c54c55c | |
| parent | 6979d892e86c86d27dbb2d49ff43e2923dd54ae1 (diff) | |
| parent | c420ad71a9f249d142754e55356f6a94de7caf82 (diff) | |
Merge "Show allowlisted widgets in communal hub" into main
19 files changed, 398 insertions, 92 deletions
diff --git a/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/CommunalGridLayout.kt b/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/CommunalGridLayout.kt index 4ed78b3b95ed..33024f764710 100644 --- a/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/CommunalGridLayout.kt +++ b/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/CommunalGridLayout.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.layout.ui.compose +import android.util.SizeF import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -54,7 +55,14 @@ fun CommunalGridLayout( Row( modifier = Modifier.height(layoutConfig.cardHeight(cardInfo.size)), ) { - cardInfo.card.Content(Modifier.fillMaxSize()) + cardInfo.card.Content( + modifier = Modifier.fillMaxSize(), + size = + SizeF( + layoutConfig.cardWidth.value, + layoutConfig.cardHeight(cardInfo.size).value, + ), + ) } } } diff --git a/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/config/CommunalGridLayoutCard.kt b/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/config/CommunalGridLayoutCard.kt index ac8aa67fa4bf..4b2a156c1dbd 100644 --- a/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/config/CommunalGridLayoutCard.kt +++ b/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/config/CommunalGridLayoutCard.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.layout.ui.compose.config +import android.util.SizeF import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -26,8 +27,11 @@ abstract class CommunalGridLayoutCard { * * To host non-Compose views, see * https://developer.android.com/jetpack/compose/migrate/interoperability-apis/views-in-compose. + * + * @param size The size given to the card. Content of the card should fill all this space, given + * that margins and paddings have been taken care of by the layout. */ - @Composable abstract fun Content(modifier: Modifier) + @Composable abstract fun Content(modifier: Modifier, size: SizeF) /** * Sizes supported by the card. diff --git a/packages/SystemUI/communal/layout/tests/src/com/android/systemui/communal/layout/CommunalLayoutEngineTest.kt b/packages/SystemUI/communal/layout/tests/src/com/android/systemui/communal/layout/CommunalLayoutEngineTest.kt index fdf65f5d5cc7..c1974caa5628 100644 --- a/packages/SystemUI/communal/layout/tests/src/com/android/systemui/communal/layout/CommunalLayoutEngineTest.kt +++ b/packages/SystemUI/communal/layout/tests/src/com/android/systemui/communal/layout/CommunalLayoutEngineTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.communal.layout +import android.util.SizeF import androidx.compose.material3.Card import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -91,7 +92,7 @@ class CommunalLayoutEngineTest { override val supportedSizes = listOf(size) @Composable - override fun Content(modifier: Modifier) { + override fun Content(modifier: Modifier, size: SizeF) { Card(modifier = modifier, content = {}) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 3d827fb5c9a6..b8fb26406801 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -1,5 +1,8 @@ package com.android.systemui.communal.ui.compose +import android.appwidget.AppWidgetHostView +import android.os.Bundle +import android.util.SizeF import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -12,9 +15,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.integerResource +import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.communal.layout.ui.compose.CommunalGridLayout import com.android.systemui.communal.layout.ui.compose.config.CommunalGridLayoutCard import com.android.systemui.communal.layout.ui.compose.config.CommunalGridLayoutConfig +import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.ui.model.CommunalContentUiModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.res.R @@ -24,6 +30,7 @@ fun CommunalHub( viewModel: CommunalViewModel, ) { val showTutorial by viewModel.showTutorialContent.collectAsState(initial = false) + val widgetContent by viewModel.widgetContent.collectAsState(initial = emptyList()) Box( modifier = modifier.fillMaxSize().background(Color.White), ) { @@ -36,7 +43,7 @@ fun CommunalHub( gridHeight = dimensionResource(R.dimen.communal_grid_height), gridColumnsPerCard = integerResource(R.integer.communal_grid_columns_per_card), ), - communalCards = if (showTutorial) tutorialContent else emptyList(), + communalCards = if (showTutorial) tutorialContent else widgetContent.map(::contentCard), ) } } @@ -58,8 +65,37 @@ private fun tutorialCard(size: CommunalGridLayoutCard.Size): CommunalGridLayoutC override val supportedSizes = listOf(size) @Composable - override fun Content(modifier: Modifier) { + override fun Content(modifier: Modifier, size: SizeF) { Card(modifier = modifier, content = {}) } } } + +private fun contentCard(model: CommunalContentUiModel): CommunalGridLayoutCard { + return object : CommunalGridLayoutCard() { + override val supportedSizes = listOf(convertToCardSize(model.size)) + override val priority = model.priority + + @Composable + override fun Content(modifier: Modifier, size: SizeF) { + AndroidView( + modifier = modifier, + factory = { + model.view.apply { + if (this is AppWidgetHostView) { + updateAppWidgetSize(Bundle(), listOf(size)) + } + } + }, + ) + } + } +} + +private fun convertToCardSize(size: CommunalContentSize): CommunalGridLayoutCard.Size { + return when (size) { + CommunalContentSize.FULL -> CommunalGridLayoutCard.Size.FULL + CommunalContentSize.HALF -> CommunalGridLayoutCard.Size.HALF + CommunalContentSize.THIRD -> CommunalGridLayoutCard.Size.THIRD + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt index f9c4f29afee9..1a214ba5a3d9 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt @@ -16,7 +16,7 @@ package com.android.systemui.communal.data.model -import com.android.systemui.communal.shared.CommunalContentSize +import com.android.systemui.communal.shared.model.CommunalContentSize /** Metadata for the default widgets */ data class CommunalWidgetMetadata( diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index f13b62fbfeb9..77025dc8839a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -20,6 +20,7 @@ import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter @@ -28,11 +29,12 @@ import android.os.UserManager import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.communal.data.model.CommunalWidgetMetadata -import com.android.systemui.communal.shared.CommunalAppWidgetInfo -import com.android.systemui.communal.shared.CommunalContentSize +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger @@ -43,6 +45,7 @@ import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map /** Encapsulates the state of widgets for communal mode. */ @@ -52,6 +55,9 @@ interface CommunalWidgetRepository { /** Widgets that are allowed to render in the glanceable hub */ val communalWidgetAllowlist: List<CommunalWidgetMetadata> + + /** A flow of information about all the communal widgets to show. */ + val communalWidgets: Flow<List<CommunalWidgetContentModel>> } @SysUISingleton @@ -67,7 +73,7 @@ constructor( private val userManager: UserManager, private val userTracker: UserTracker, @CommunalLog logBuffer: LogBuffer, - featureFlags: FeatureFlags, + featureFlags: FeatureFlagsClassic, ) : CommunalWidgetRepository { companion object { const val TAG = "CommunalWidgetRepository" @@ -88,49 +94,59 @@ constructor( // Widgets that should be rendered in communal mode. private val widgets: HashMap<Int, CommunalAppWidgetInfo> = hashMapOf() - private val isUserUnlocked: Flow<Boolean> = callbackFlow { - if (!featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)) { - awaitClose() - } - - fun isUserUnlockingOrUnlocked(): Boolean { - return userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) - } - - fun send() { - trySendWithFailureLogging(isUserUnlockingOrUnlocked(), TAG) - } + private val isUserUnlocked: Flow<Boolean> = + callbackFlow { + if (!communalRepository.isCommunalEnabled) { + awaitClose() + } - if (isUserUnlockingOrUnlocked()) { - send() - awaitClose() - } else { - val receiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - send() - } + fun isUserUnlockingOrUnlocked(): Boolean { + return userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) } - broadcastDispatcher.registerReceiver( - receiver, - IntentFilter(Intent.ACTION_USER_UNLOCKED), - ) + fun send() { + trySendWithFailureLogging(isUserUnlockingOrUnlocked(), TAG) + } - awaitClose { broadcastDispatcher.unregisterReceiver(receiver) } + if (isUserUnlockingOrUnlocked()) { + send() + awaitClose() + } else { + val receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + send() + } + } + + broadcastDispatcher.registerReceiver( + receiver, + IntentFilter(Intent.ACTION_USER_UNLOCKED), + ) + + awaitClose { broadcastDispatcher.unregisterReceiver(receiver) } + } + } + .distinctUntilChanged() + + private val isHostActive: Flow<Boolean> = + isUserUnlocked.map { + if (it) { + startListening() + true + } else { + stopListening() + clearWidgets() + false + } } - } override val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> = - isUserUnlocked.map { isUserUnlocked -> - if (!isUserUnlocked) { - clearWidgets() - stopListening() + isHostActive.map { isHostActive -> + if (!isHostActive || !featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)) { return@map null } - startListening() - val providerInfo = appWidgetManager.installedProviders.find { it.loadLabel(packageManager).equals(WIDGET_LABEL) @@ -144,6 +160,42 @@ constructor( return@map addWidget(providerInfo) } + override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = + isHostActive.map { isHostActive -> + if (!isHostActive) { + return@map emptyList() + } + + // The allowlist should be fetched from the local database with all the metadata tied to + // a widget, including an appWidgetId if it has been bound. Before the database is set + // up, we are going to use the app widget host as the source of truth for bound widgets, + // and rebind each time on boot. + + // Remove all previously bound widgets. + appWidgetHost.appWidgetIds.forEach { appWidgetHost.deleteAppWidgetId(it) } + + val inventory = mutableListOf<CommunalWidgetContentModel>() + + // Bind all widgets from the allowlist. + communalWidgetAllowlist.forEach { + val id = appWidgetHost.allocateAppWidgetId() + appWidgetManager.bindAppWidgetId( + id, + ComponentName.unflattenFromString(it.componentName), + ) + + inventory.add( + CommunalWidgetContentModel( + appWidgetId = id, + providerInfo = appWidgetManager.getAppWidgetInfo(id), + priority = it.priority, + ) + ) + } + + return@map inventory.toList() + } + private fun getWidgetAllowlist(): List<CommunalWidgetMetadata> { val componentNames = applicationContext.resources.getStringArray(R.array.config_communalWidgetAllowlist) @@ -151,7 +203,7 @@ constructor( CommunalWidgetMetadata( componentName = name, priority = componentNames.size - index, - sizes = listOf(CommunalContentSize.HALF) + sizes = listOf(CommunalContentSize.HALF), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 04bb6ae75e60..62387079ab35 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -18,7 +18,8 @@ package com.android.systemui.communal.domain.interactor import com.android.systemui.communal.data.repository.CommunalRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository -import com.android.systemui.communal.shared.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -38,4 +39,12 @@ constructor( /** A flow of info about the widget to be displayed, or null if widget is unavailable. */ val appWidgetInfo: Flow<CommunalAppWidgetInfo?> = widgetRepository.stopwatchAppWidgetInfo + + /** + * A flow of information about widgets to be shown in communal hub. + * + * Currently only showing persistent widgets that have been bound to the app widget service + * (have an allocated id). + */ + val widgetContent: Flow<List<CommunalWidgetContentModel>> = widgetRepository.communalWidgets } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalContentSize.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalContentSize.kt deleted file mode 100644 index 0bd7d86c972d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalContentSize.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.android.systemui.communal.shared - -/** Supported sizes for communal content in the layout grid. */ -enum class CommunalContentSize { - FULL, - HALF, - THIRD, -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalAppWidgetInfo.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalAppWidgetInfo.kt index 0803a01b93b8..109ed2da7e48 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalAppWidgetInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalAppWidgetInfo.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.communal.shared +package com.android.systemui.communal.shared.model import android.appwidget.AppWidgetProviderInfo diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentCategory.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentCategory.kt new file mode 100644 index 000000000000..7f05b9cd4943 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentCategory.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 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.shared.model + +enum class CommunalContentCategory { + /** The content persists in the communal hub until removed by the user. */ + PERSISTENT, + + /** The content temporarily shows up in the communal hub when certain conditions are met. */ + TRANSIENT, +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt new file mode 100644 index 000000000000..39a6476929ed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 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.shared.model + +/** Supported sizes for communal content in the layout grid. */ +enum class CommunalContentSize { + /** Content takes the full height of the column. */ + FULL, + + /** Content takes half of the height of the column. */ + HALF, + + /** Content takes a third of the height of the column. */ + THIRD, +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt new file mode 100644 index 000000000000..e141dc40477c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 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.shared.model + +import android.appwidget.AppWidgetProviderInfo + +/** Encapsulates data for a communal widget. */ +data class CommunalWidgetContentModel( + val appWidgetId: Int, + val providerInfo: AppWidgetProviderInfo, + val priority: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt index 2a08d7f6bc28..0daf7b5610e8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt @@ -20,7 +20,7 @@ import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.content.Context import android.util.SizeF -import com.android.systemui.communal.shared.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo import com.android.systemui.communal.ui.view.CommunalWidgetWrapper import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/model/CommunalContentUiModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/model/CommunalContentUiModel.kt new file mode 100644 index 000000000000..98060dc1dceb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/model/CommunalContentUiModel.kt @@ -0,0 +1,15 @@ +package com.android.systemui.communal.ui.model + +import android.view.View +import com.android.systemui.communal.shared.model.CommunalContentSize + +/** + * Encapsulates data for a communal content that holds a view. + * + * This model stays in the UI layer. + */ +data class CommunalContentUiModel( + val view: View, + val size: CommunalContentSize, + val priority: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index ddeb1d67b945..25c64eafe255 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -16,17 +16,43 @@ package com.android.systemui.communal.ui.viewmodel +import android.appwidget.AppWidgetHost +import android.content.Context +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor +import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.ui.model.CommunalContentUiModel import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map @SysUISingleton class CommunalViewModel @Inject constructor( + @Application private val context: Context, + private val appWidgetHost: AppWidgetHost, + communalInteractor: CommunalInteractor, tutorialInteractor: CommunalTutorialInteractor, ) { /** Whether communal hub should show tutorial content. */ val showTutorialContent: Flow<Boolean> = tutorialInteractor.isTutorialAvailable + + /** List of widgets to be displayed in the communal hub. */ + val widgetContent: Flow<List<CommunalContentUiModel>> = + communalInteractor.widgetContent.map { + it.map { + // TODO(b/306406256): As adding and removing widgets functionalities are + // supported, cache the host views so they're not recreated each time. + val hostView = appWidgetHost.createView(context, it.appWidgetId, it.providerInfo) + return@map CommunalContentUiModel( + view = hostView, + priority = it.priority, + // All widgets have HALF size. + size = CommunalContentSize.HALF, + ) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt index 8fba342c49be..d7bbea64332e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.communal.ui.viewmodel import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.shared.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import javax.inject.Inject diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index 91409a376556..fcb191b4cbd6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -12,9 +12,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.communal.data.model.CommunalWidgetMetadata -import com.android.systemui.communal.shared.CommunalContentSize +import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.FakeLogBuffer @@ -38,6 +39,7 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.anyInt +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @@ -58,10 +60,16 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker - @Mock private lateinit var featureFlags: FeatureFlags + @Mock private lateinit var featureFlags: FeatureFlagsClassic @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo + @Mock private lateinit var providerInfoA: AppWidgetProviderInfo + + @Mock private lateinit var providerInfoB: AppWidgetProviderInfo + + @Mock private lateinit var providerInfoC: AppWidgetProviderInfo + private lateinit var communalRepository: FakeCommunalRepository private lateinit var logBuffer: LogBuffer @@ -70,29 +78,34 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { private val testScope = TestScope(testDispatcher) + private val fakeAllowlist = + listOf( + "com.android.fake/WidgetProviderA", + "com.android.fake/WidgetProviderB", + "com.android.fake/WidgetProviderC", + ) + @Before fun setUp() { MockitoAnnotations.initMocks(this) logBuffer = FakeLogBuffer.Factory.create() - - featureFlagEnabled(true) communalRepository = FakeCommunalRepository() - communalRepository.setIsCommunalEnabled(true) - overrideResource( - R.array.config_communalWidgetAllowlist, - arrayOf(componentName1, componentName2) - ) + communalEnabled(true) + widgetOnKeyguardEnabled(true) + setAppWidgetIds(emptyList()) + + overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray()) whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch") whenever(userTracker.userHandle).thenReturn(userHandle) } @Test - fun broadcastReceiver_featureDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = + fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = testScope.runTest { - featureFlagEnabled(false) + communalEnabled(false) val repository = initCommunalWidgetRepository() collectLastValue(repository.stopwatchAppWidgetInfo)() verifyBroadcastReceiverNeverRegistered() @@ -129,7 +142,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { job.cancel() runCurrent() - Mockito.verify(broadcastDispatcher).unregisterReceiver(receiver) + verify(broadcastDispatcher).unregisterReceiver(receiver) } @Test @@ -166,7 +179,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { installedProviders(listOf(stopwatchProviderInfo)) val repository = initCommunalWidgetRepository() collectLastValue(repository.stopwatchAppWidgetInfo)() - Mockito.verify(appWidgetHost).allocateAppWidgetId() + verify(appWidgetHost).allocateAppWidgetId() } @Test @@ -185,8 +198,8 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { // Verify app widget id allocated assertThat(lastStopwatchProviderInfo()?.appWidgetId).isEqualTo(123456) - Mockito.verify(appWidgetHost).allocateAppWidgetId() - Mockito.verify(appWidgetHost, Mockito.never()).deleteAppWidgetId(anyInt()) + verify(appWidgetHost).allocateAppWidgetId() + verify(appWidgetHost, Mockito.never()).deleteAppWidgetId(anyInt()) // User locked again userUnlocked(false) @@ -194,7 +207,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { // Verify app widget id deleted assertThat(lastStopwatchProviderInfo()).isNull() - Mockito.verify(appWidgetHost).deleteAppWidgetId(123456) + verify(appWidgetHost).deleteAppWidgetId(123456) } @Test @@ -203,13 +216,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { userUnlocked(false) val repository = initCommunalWidgetRepository() collectLastValue(repository.stopwatchAppWidgetInfo)() - Mockito.verify(appWidgetHost, Mockito.never()).startListening() + verify(appWidgetHost, Mockito.never()).startListening() userUnlocked(true) broadcastReceiverUpdate() collectLastValue(repository.stopwatchAppWidgetInfo)() - Mockito.verify(appWidgetHost).startListening() + verify(appWidgetHost).startListening() } @Test @@ -223,14 +236,14 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { broadcastReceiverUpdate() collectLastValue(repository.stopwatchAppWidgetInfo)() - Mockito.verify(appWidgetHost).startListening() - Mockito.verify(appWidgetHost, Mockito.never()).stopListening() + verify(appWidgetHost).startListening() + verify(appWidgetHost, Mockito.never()).stopListening() userUnlocked(false) broadcastReceiverUpdate() collectLastValue(repository.stopwatchAppWidgetInfo)() - Mockito.verify(appWidgetHost).stopListening() + verify(appWidgetHost).stopListening() } @Test @@ -241,21 +254,80 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { assertThat( listOf( CommunalWidgetMetadata( - componentName = componentName1, + componentName = fakeAllowlist[0], + priority = 3, + sizes = listOf(CommunalContentSize.HALF), + ), + CommunalWidgetMetadata( + componentName = fakeAllowlist[1], priority = 2, - sizes = listOf(CommunalContentSize.HALF) + sizes = listOf(CommunalContentSize.HALF), ), CommunalWidgetMetadata( - componentName = componentName2, + componentName = fakeAllowlist[2], priority = 1, - sizes = listOf(CommunalContentSize.HALF) - ) + sizes = listOf(CommunalContentSize.HALF), + ), ) ) .containsExactly(*communalWidgetAllowlist.toTypedArray()) } } + // This behavior is temporary before the local database is set up. + @Test + fun communalWidgets_withPreviouslyBoundWidgets_removeEachBinding() = + testScope.runTest { + whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(1, 2, 3) + setAppWidgetIds(listOf(1, 2, 3)) + whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA) + userUnlocked(true) + + val repository = initCommunalWidgetRepository() + + collectLastValue(repository.communalWidgets)() + + verify(appWidgetHost).deleteAppWidgetId(1) + verify(appWidgetHost).deleteAppWidgetId(2) + verify(appWidgetHost).deleteAppWidgetId(3) + } + + @Test + fun communalWidgets_allowlistNotEmpty_bindEachWidgetFromTheAllowlist() = + testScope.runTest { + whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(0, 1, 2) + userUnlocked(true) + + whenever(appWidgetManager.getAppWidgetInfo(0)).thenReturn(providerInfoA) + whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfoB) + whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfoC) + + val repository = initCommunalWidgetRepository() + + val inventory by collectLastValue(repository.communalWidgets) + + assertThat( + listOf( + CommunalWidgetContentModel( + appWidgetId = 0, + providerInfo = providerInfoA, + priority = 3, + ), + CommunalWidgetContentModel( + appWidgetId = 1, + providerInfo = providerInfoB, + priority = 2, + ), + CommunalWidgetContentModel( + appWidgetId = 2, + providerInfo = providerInfoC, + priority = 1, + ), + ) + ) + .containsExactly(*inventory!!.toTypedArray()) + } + private fun initCommunalWidgetRepository(): CommunalWidgetRepositoryImpl { return CommunalWidgetRepositoryImpl( context, @@ -272,7 +344,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } private fun verifyBroadcastReceiverRegistered() { - Mockito.verify(broadcastDispatcher) + verify(broadcastDispatcher) .registerReceiver( any(), any(), @@ -284,7 +356,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } private fun verifyBroadcastReceiverNeverRegistered() { - Mockito.verify(broadcastDispatcher, Mockito.never()) + verify(broadcastDispatcher, Mockito.never()) .registerReceiver( any(), any(), @@ -297,7 +369,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { private fun broadcastReceiverUpdate(): BroadcastReceiver { val broadcastReceiverCaptor = kotlinArgumentCaptor<BroadcastReceiver>() - Mockito.verify(broadcastDispatcher) + verify(broadcastDispatcher) .registerReceiver( broadcastReceiverCaptor.capture(), any(), @@ -310,7 +382,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { return broadcastReceiverCaptor.value } - private fun featureFlagEnabled(enabled: Boolean) { + private fun communalEnabled(enabled: Boolean) { + communalRepository.setIsCommunalEnabled(enabled) + } + + private fun widgetOnKeyguardEnabled(enabled: Boolean) { whenever(featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)).thenReturn(enabled) } @@ -322,8 +398,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever(appWidgetManager.installedProviders).thenReturn(providers) } - companion object { - const val componentName1 = "component name 1" - const val componentName2 = "component name 2" + private fun setAppWidgetIds(ids: List<Int>) { + whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index cdc42e096830..8e21f294a361 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -22,7 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository -import com.android.systemui.communal.shared.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo import com.android.systemui.coroutines.collectLastValue import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index 30132f7747b7..08adda32eb6d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -1,7 +1,8 @@ package com.android.systemui.communal.data.repository import com.android.systemui.communal.data.model.CommunalWidgetMetadata -import com.android.systemui.communal.shared.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -11,7 +12,14 @@ class FakeCommunalWidgetRepository : CommunalWidgetRepository { override val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> = _stopwatchAppWidgetInfo override var communalWidgetAllowlist: List<CommunalWidgetMetadata> = emptyList() + private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList()) + override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets + fun setStopwatchAppWidgetInfo(appWidgetInfo: CommunalAppWidgetInfo) { _stopwatchAppWidgetInfo.value = appWidgetInfo } + + fun setCommunalWidgets(inventory: List<CommunalWidgetContentModel>) { + _communalWidgets.value = inventory + } } |