summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Darrell Shi <darrellshi@google.com> 2023-08-11 16:25:02 +0000
committer Darrell Shi <darrellshi@google.com> 2023-08-15 20:58:54 +0000
commit1d9496a9bb1e4061e47943d24f566a84b581f266 (patch)
treed7d1307bcc4c666679a9570a29e4b676f636dc05
parent886c5e2463af3eb87dfd1c9dededa32b6ccf126e (diff)
Show a Stopwatch widget in communal blueprint
This change introduces a keyguard section for widgets in communal mode, and adds a Stopwatch widget to it, laid out at the bottom right corner. Bug: 288275889 Test: adb shell cmd statusbar flag widget_on_keyguard on && \ adb shell setprop ctl.restart zygote && \ adb shell cmd statusbar blueprint communal Test: atest KeyguardWidgetRepositoryImplTest Test: atest CommunalInteractorTest Test: atest DefaultCommunalBlueprintTest Change-Id: Ib8a5a18bd78778c0915ef1796c594e170f2a0b64
-rw-r--r--packages/SystemUI/res/values/ids.xml3
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt170
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt49
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/shared/CommunalAppWidgetInfo.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt80
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalWidgetViewBinder.kt70
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprint.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/DefaultCommunalWidgetSection.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/CommunalLog.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt285
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt70
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprintTest.kt15
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt15
19 files changed, 988 insertions, 8 deletions
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index d2cb475ad2b0..4e72518bc613 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -219,4 +219,7 @@
<!-- Privacy dialog -->
<item type="id" name="privacy_dialog_close_app_button" />
<item type="id" name="privacy_dialog_manage_app_button" />
+
+ <!-- Communal mode -->
+ <item type="id" name="communal_widget_wrapper" />
</resources>
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
new file mode 100644
index 000000000000..e2a7d077a32c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.data.repository
+
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.os.UserManager
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.communal.shared.CommunalAppWidgetInfo
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.map
+
+/** Encapsulates the state of widgets for communal mode. */
+interface CommunalWidgetRepository {
+ /** A flow of provider info for the stopwatch widget, or null if widget is unavailable. */
+ val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?>
+}
+
+@SysUISingleton
+class CommunalWidgetRepositoryImpl
+@Inject
+constructor(
+ private val appWidgetManager: AppWidgetManager,
+ private val appWidgetHost: AppWidgetHost,
+ broadcastDispatcher: BroadcastDispatcher,
+ private val packageManager: PackageManager,
+ private val userManager: UserManager,
+ private val userTracker: UserTracker,
+ @CommunalLog logBuffer: LogBuffer,
+ featureFlags: FeatureFlags,
+) : CommunalWidgetRepository {
+ companion object {
+ const val TAG = "CommunalWidgetRepository"
+ const val WIDGET_LABEL = "Stopwatch"
+ }
+
+ private val logger = Logger(logBuffer, TAG)
+
+ // Whether the [AppWidgetHost] is listening for updates.
+ private var isHostListening = false
+
+ // 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)
+ }
+
+ 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) }
+ }
+ }
+
+ override val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> =
+ isUserUnlocked.map { isUserUnlocked ->
+ if (!isUserUnlocked) {
+ clearWidgets()
+ stopListening()
+ return@map null
+ }
+
+ startListening()
+
+ val providerInfo =
+ appWidgetManager.installedProviders.find {
+ it.loadLabel(packageManager).equals(WIDGET_LABEL)
+ }
+
+ if (providerInfo == null) {
+ logger.w("Cannot find app widget: $WIDGET_LABEL")
+ return@map null
+ }
+
+ return@map addWidget(providerInfo)
+ }
+
+ private fun startListening() {
+ if (isHostListening) {
+ return
+ }
+
+ appWidgetHost.startListening()
+ isHostListening = true
+ }
+
+ private fun stopListening() {
+ if (!isHostListening) {
+ return
+ }
+
+ appWidgetHost.stopListening()
+ isHostListening = false
+ }
+
+ private fun addWidget(providerInfo: AppWidgetProviderInfo): CommunalAppWidgetInfo {
+ val existing = widgets.values.firstOrNull { it.providerInfo == providerInfo }
+ if (existing != null) {
+ return existing
+ }
+
+ val appWidgetId = appWidgetHost.allocateAppWidgetId()
+ val widget =
+ CommunalAppWidgetInfo(
+ providerInfo,
+ appWidgetId,
+ )
+ widgets[appWidgetId] = widget
+ return widget
+ }
+
+ private fun clearWidgets() {
+ widgets.keys.forEach { appWidgetId -> appWidgetHost.deleteAppWidgetId(appWidgetId) }
+ widgets.clear()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
new file mode 100644
index 000000000000..3d1185b79275
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.data.repository
+
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+
+@Module
+interface CommunalWidgetRepositoryModule {
+ companion object {
+ private const val APP_WIDGET_HOST_ID = 116
+
+ @SysUISingleton
+ @Provides
+ fun provideAppWidgetManager(@Application context: Context): AppWidgetManager {
+ return AppWidgetManager.getInstance(context)
+ }
+
+ @SysUISingleton
+ @Provides
+ fun provideAppWidgetHost(@Application context: Context): AppWidgetHost {
+ return AppWidgetHost(context, APP_WIDGET_HOST_ID)
+ }
+ }
+
+ @Binds
+ fun communalWidgetRepository(impl: CommunalWidgetRepositoryImpl): CommunalWidgetRepository
+}
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
new file mode 100644
index 000000000000..6dc305e6f826
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.domain.interactor
+
+import com.android.systemui.communal.data.repository.CommunalWidgetRepository
+import com.android.systemui.communal.shared.CommunalAppWidgetInfo
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Encapsulates business-logic related to communal mode. */
+@SysUISingleton
+class CommunalInteractor
+@Inject
+constructor(
+ widgetRepository: CommunalWidgetRepository,
+) {
+ /** A flow of info about the widget to be displayed, or null if widget is unavailable. */
+ val appWidgetInfo: Flow<CommunalAppWidgetInfo?> = widgetRepository.stopwatchAppWidgetInfo
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalAppWidgetInfo.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalAppWidgetInfo.kt
new file mode 100644
index 000000000000..0803a01b93b8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalAppWidgetInfo.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
+
+import android.appwidget.AppWidgetProviderInfo
+
+/** A data class that stores info about an app widget that displays in communal mode. */
+data class CommunalAppWidgetInfo(
+ val providerInfo: AppWidgetProviderInfo,
+ val appWidgetId: 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
new file mode 100644
index 000000000000..2a08d7f6bc28
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.ui.adapter
+
+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.ui.view.CommunalWidgetWrapper
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Transforms a [CommunalAppWidgetInfo] to a view that renders the widget. */
+class CommunalWidgetViewAdapter
+@Inject
+constructor(
+ @Application private val context: Context,
+ private val appWidgetManager: AppWidgetManager,
+ private val appWidgetHost: AppWidgetHost,
+ @CommunalLog logBuffer: LogBuffer,
+) {
+ companion object {
+ private const val TAG = "CommunalWidgetViewAdapter"
+ }
+
+ private val logger = Logger(logBuffer, TAG)
+
+ fun adapt(providerInfoFlow: Flow<CommunalAppWidgetInfo?>): Flow<CommunalWidgetWrapper?> =
+ providerInfoFlow.map {
+ if (it == null) {
+ return@map null
+ }
+
+ val appWidgetId = it.appWidgetId
+ val providerInfo = it.providerInfo
+
+ if (appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, providerInfo.provider)) {
+ logger.d("Success binding app widget id: $appWidgetId")
+ return@map CommunalWidgetWrapper(context).apply {
+ addView(
+ appWidgetHost.createView(context, appWidgetId, providerInfo).apply {
+ // Set the widget to minimum width and height
+ updateAppWidgetSize(
+ appWidgetManager.getAppWidgetOptions(appWidgetId),
+ listOf(
+ SizeF(
+ providerInfo.minResizeWidth.toFloat(),
+ providerInfo.minResizeHeight.toFloat()
+ )
+ )
+ )
+ }
+ )
+ }
+ } else {
+ logger.w("Failed binding app widget id")
+ return@map null
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalWidgetViewBinder.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalWidgetViewBinder.kt
new file mode 100644
index 000000000000..1b6d3a8095f0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalWidgetViewBinder.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.ui.binder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.communal.ui.adapter.CommunalWidgetViewAdapter
+import com.android.systemui.communal.ui.view.CommunalWidgetWrapper
+import com.android.systemui.communal.ui.viewmodel.CommunalWidgetViewModel
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.launch
+
+/** Binds [CommunalWidgetViewModel] to the keyguard root view. */
+object CommunalWidgetViewBinder {
+
+ @JvmStatic
+ fun bind(
+ rootView: KeyguardRootView,
+ viewModel: CommunalWidgetViewModel,
+ adapter: CommunalWidgetViewAdapter,
+ keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+ ) {
+ rootView.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ adapter.adapt(viewModel.appWidgetInfo).collect {
+ val oldView =
+ rootView.findViewById<CommunalWidgetWrapper>(
+ R.id.communal_widget_wrapper
+ )
+ var dirty = false
+
+ if (oldView != null) {
+ rootView.removeView(oldView)
+ dirty = true
+ }
+
+ if (it != null) {
+ rootView.addView(it)
+ dirty = true
+ }
+
+ if (dirty) {
+ keyguardBlueprintInteractor.refreshBlueprint()
+ }
+ }
+ }
+
+ launch { viewModel.alpha.collect { rootView.alpha = it } }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt
new file mode 100644
index 000000000000..560f4fac048f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.LinearLayout
+import com.android.systemui.R
+
+/** Wraps around a widget rendered in communal mode. */
+class CommunalWidgetWrapper(context: Context, attrs: AttributeSet? = null) :
+ LinearLayout(context, attrs) {
+ init {
+ id = R.id.communal_widget_wrapper
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprint.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprint.kt
index bf402749add7..c3369da68821 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprint.kt
@@ -17,6 +17,7 @@
package com.android.systemui.communal.ui.view.layout.blueprints
import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.communal.ui.view.layout.sections.DefaultCommunalWidgetSection
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.KeyguardBlueprint
import javax.inject.Inject
@@ -24,10 +25,16 @@ import javax.inject.Inject
/** Blueprint for communal mode. */
@SysUISingleton
@JvmSuppressWildcards
-class DefaultCommunalBlueprint @Inject constructor() : KeyguardBlueprint {
+class DefaultCommunalBlueprint
+@Inject
+constructor(
+ private val defaultCommunalWidgetSection: DefaultCommunalWidgetSection,
+) : KeyguardBlueprint {
override val id: String = COMMUNAL
- override fun apply(constraintSet: ConstraintSet) {}
+ override fun apply(constraintSet: ConstraintSet) {
+ defaultCommunalWidgetSection.apply(constraintSet)
+ }
companion object {
const val COMMUNAL = "communal"
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/DefaultCommunalWidgetSection.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/DefaultCommunalWidgetSection.kt
new file mode 100644
index 000000000000..b0e3132a1fc7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/DefaultCommunalWidgetSection.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.ui.view.layout.sections
+
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import com.android.systemui.R
+import com.android.systemui.keyguard.data.repository.KeyguardSection
+import javax.inject.Inject
+
+class DefaultCommunalWidgetSection @Inject constructor() : KeyguardSection {
+ private val widgetAreaViewId = R.id.communal_widget_wrapper
+
+ override fun apply(constraintSet: ConstraintSet) {
+ constraintSet.apply {
+ constrainWidth(widgetAreaViewId, WRAP_CONTENT)
+ constrainHeight(widgetAreaViewId, WRAP_CONTENT)
+ connect(widgetAreaViewId, BOTTOM, PARENT_ID, BOTTOM)
+ connect(widgetAreaViewId, END, PARENT_ID, END)
+ }
+ }
+}
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
new file mode 100644
index 000000000000..8fba342c49be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.ui.viewmodel
+
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.shared.CommunalAppWidgetInfo
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class CommunalWidgetViewModel
+@Inject
+constructor(
+ communalInteractor: CommunalInteractor,
+ keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
+) {
+ /** An observable for the alpha level for the communal widget area. */
+ val alpha: Flow<Float> = keyguardBottomAreaViewModel.alpha
+
+ /** A flow of info about the widget to be displayed, or null if widget is unavailable. */
+ val appWidgetInfo: Flow<CommunalAppWidgetInfo?> = communalInteractor.appWidgetInfo
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 8e323d8140d5..9b323ee9a3f3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -24,9 +24,13 @@ import com.android.keyguard.KeyguardStatusViewController
import com.android.keyguard.dagger.KeyguardStatusViewComponent
import com.android.systemui.CoreStartable
import com.android.systemui.R
+import com.android.systemui.communal.ui.adapter.CommunalWidgetViewAdapter
+import com.android.systemui.communal.ui.binder.CommunalWidgetViewBinder
+import com.android.systemui.communal.ui.viewmodel.CommunalWidgetViewModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.ui.binder.KeyguardAmbientIndicationAreaViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
@@ -83,6 +87,9 @@ constructor(
private val keyguardBlueprintCommandListener: KeyguardBlueprintCommandListener,
private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel,
private val keyguardStatusViewComponentFactory: KeyguardStatusViewComponent.Factory,
+ private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+ private val communalWidgetViewModel: CommunalWidgetViewModel,
+ private val communalWidgetViewAdapter: CommunalWidgetViewAdapter,
) : CoreStartable {
private var rootViewHandle: DisposableHandle? = null
@@ -106,6 +113,7 @@ constructor(
bindRightShortcut()
bindAmbientIndicationArea()
bindSettingsPopupMenu()
+ bindCommunalWidgetArea()
KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel)
keyguardBlueprintCommandListener.start()
@@ -279,6 +287,19 @@ constructor(
}
}
+ private fun bindCommunalWidgetArea() {
+ if (!featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)) {
+ return
+ }
+
+ CommunalWidgetViewBinder.bind(
+ keyguardRootView,
+ communalWidgetViewModel,
+ communalWidgetViewAdapter,
+ keyguardBlueprintInteractor,
+ )
+ }
+
/**
* Temporary, to allow NotificationPanelViewController to use the same instance while code is
* migrated: b/288242803
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 9a44230cf185..13dfe24e7e19 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -37,6 +37,7 @@ import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.classifier.FalsingModule;
+import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -75,11 +76,12 @@ import com.android.systemui.util.time.SystemClock;
import com.android.systemui.wallpapers.data.repository.WallpaperRepository;
import com.android.wm.shell.keyguard.KeyguardTransitions;
-import java.util.concurrent.Executor;
-
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
+
+import java.util.concurrent.Executor;
+
import kotlinx.coroutines.CoroutineDispatcher;
/**
@@ -91,6 +93,7 @@ import kotlinx.coroutines.CoroutineDispatcher;
KeyguardStatusViewComponent.class,
KeyguardUserSwitcherComponent.class},
includes = {
+ CommunalWidgetRepositoryModule.class,
FalsingModule.class,
KeyguardDataQuickAffordanceModule.class,
KeyguardRepositoryModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalLog.kt
new file mode 100644
index 000000000000..afb18f1f23c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalLog.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for communal-related logging. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class CommunalLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index cc1504a1df97..cbb29cd9ea63 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -489,6 +489,16 @@ public class LogModule {
return factory.create("DreamLog", 250);
}
+ /**
+ * Provides a {@link LogBuffer} for communal-related logs.
+ */
+ @Provides
+ @SysUISingleton
+ @CommunalLog
+ public static LogBuffer provideCommunalLogBuffer(LogBufferFactory factory) {
+ return factory.create("CommunalLog", 250);
+ }
+
/** Provides a {@link LogBuffer} for display metrics related logs. */
@Provides
@SysUISingleton
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
new file mode 100644
index 000000000000..3df9cbb29e4a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -0,0 +1,285 @@
+package com.android.systemui.communal.data.repository
+
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.BroadcastReceiver
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.FakeLogBuffer
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+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
+import org.mockito.Mockito.anyInt
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
+ @Mock private lateinit var appWidgetManager: AppWidgetManager
+
+ @Mock private lateinit var appWidgetHost: AppWidgetHost
+
+ @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+
+ @Mock private lateinit var packageManager: PackageManager
+
+ @Mock private lateinit var userManager: UserManager
+
+ @Mock private lateinit var userHandle: UserHandle
+
+ @Mock private lateinit var userTracker: UserTracker
+
+ @Mock private lateinit var featureFlags: FeatureFlags
+
+ @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo
+
+ private lateinit var logBuffer: LogBuffer
+
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ logBuffer = FakeLogBuffer.Factory.create()
+
+ featureFlagEnabled(true)
+ whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch")
+ whenever(userTracker.userHandle).thenReturn(userHandle)
+ }
+
+ @Test
+ fun broadcastReceiver_featureDisabled_doNotRegisterUserUnlockedBroadcastReceiver() =
+ testScope.runTest {
+ featureFlagEnabled(false)
+ val repository = initCommunalWidgetRepository()
+ collectLastValue(repository.stopwatchAppWidgetInfo)()
+ verifyBroadcastReceiverNeverRegistered()
+ }
+
+ @Test
+ fun broadcastReceiver_featureEnabledAndUserUnlocked_doNotRegisterBroadcastReceiver() =
+ testScope.runTest {
+ userUnlocked(true)
+ val repository = initCommunalWidgetRepository()
+ collectLastValue(repository.stopwatchAppWidgetInfo)()
+ verifyBroadcastReceiverNeverRegistered()
+ }
+
+ @Test
+ fun broadcastReceiver_featureEnabledAndUserLocked_registerBroadcastReceiver() =
+ testScope.runTest {
+ userUnlocked(false)
+ val repository = initCommunalWidgetRepository()
+ collectLastValue(repository.stopwatchAppWidgetInfo)()
+ verifyBroadcastReceiverRegistered()
+ }
+
+ @Test
+ fun broadcastReceiver_whenFlowFinishes_unregisterBroadcastReceiver() =
+ testScope.runTest {
+ userUnlocked(false)
+ val repository = initCommunalWidgetRepository()
+
+ val job = launch { repository.stopwatchAppWidgetInfo.collect() }
+ runCurrent()
+ val receiver = broadcastReceiverUpdate()
+
+ job.cancel()
+ runCurrent()
+
+ Mockito.verify(broadcastDispatcher).unregisterReceiver(receiver)
+ }
+
+ @Test
+ fun stopwatch_whenUserUnlocks_receiveProviderInfo() =
+ testScope.runTest {
+ userUnlocked(false)
+ val repository = initCommunalWidgetRepository()
+ val lastStopwatchProviderInfo = collectLastValue(repository.stopwatchAppWidgetInfo)
+ assertThat(lastStopwatchProviderInfo()).isNull()
+
+ userUnlocked(true)
+ installedProviders(listOf(stopwatchProviderInfo))
+ broadcastReceiverUpdate()
+
+ assertThat(lastStopwatchProviderInfo()?.providerInfo).isEqualTo(stopwatchProviderInfo)
+ }
+
+ @Test
+ fun stopwatch_userUnlockedButWidgetNotInstalled_noProviderInfo() =
+ testScope.runTest {
+ userUnlocked(true)
+ installedProviders(listOf())
+
+ val repository = initCommunalWidgetRepository()
+
+ val lastStopwatchProviderInfo = collectLastValue(repository.stopwatchAppWidgetInfo)
+ assertThat(lastStopwatchProviderInfo()).isNull()
+ }
+
+ @Test
+ fun appWidgetId_providerInfoAvailable_allocateAppWidgetId() =
+ testScope.runTest {
+ userUnlocked(true)
+ installedProviders(listOf(stopwatchProviderInfo))
+ val repository = initCommunalWidgetRepository()
+ collectLastValue(repository.stopwatchAppWidgetInfo)()
+ Mockito.verify(appWidgetHost).allocateAppWidgetId()
+ }
+
+ @Test
+ fun appWidgetId_userLockedAgainAfterProviderInfoAvailable_deleteAppWidgetId() =
+ testScope.runTest {
+ whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(123456)
+ userUnlocked(false)
+ val repository = initCommunalWidgetRepository()
+ val lastStopwatchProviderInfo = collectLastValue(repository.stopwatchAppWidgetInfo)
+ assertThat(lastStopwatchProviderInfo()).isNull()
+
+ // User unlocks
+ userUnlocked(true)
+ installedProviders(listOf(stopwatchProviderInfo))
+ broadcastReceiverUpdate()
+
+ // Verify app widget id allocated
+ assertThat(lastStopwatchProviderInfo()?.appWidgetId).isEqualTo(123456)
+ Mockito.verify(appWidgetHost).allocateAppWidgetId()
+ Mockito.verify(appWidgetHost, Mockito.never()).deleteAppWidgetId(anyInt())
+
+ // User locked again
+ userUnlocked(false)
+ broadcastReceiverUpdate()
+
+ // Verify app widget id deleted
+ assertThat(lastStopwatchProviderInfo()).isNull()
+ Mockito.verify(appWidgetHost).deleteAppWidgetId(123456)
+ }
+
+ @Test
+ fun appWidgetHost_userUnlocked_startListening() =
+ testScope.runTest {
+ userUnlocked(false)
+ val repository = initCommunalWidgetRepository()
+ collectLastValue(repository.stopwatchAppWidgetInfo)()
+ Mockito.verify(appWidgetHost, Mockito.never()).startListening()
+
+ userUnlocked(true)
+ broadcastReceiverUpdate()
+ collectLastValue(repository.stopwatchAppWidgetInfo)()
+
+ Mockito.verify(appWidgetHost).startListening()
+ }
+
+ @Test
+ fun appWidgetHost_userLockedAgain_stopListening() =
+ testScope.runTest {
+ userUnlocked(false)
+ val repository = initCommunalWidgetRepository()
+ collectLastValue(repository.stopwatchAppWidgetInfo)()
+
+ userUnlocked(true)
+ broadcastReceiverUpdate()
+ collectLastValue(repository.stopwatchAppWidgetInfo)()
+
+ Mockito.verify(appWidgetHost).startListening()
+ Mockito.verify(appWidgetHost, Mockito.never()).stopListening()
+
+ userUnlocked(false)
+ broadcastReceiverUpdate()
+ collectLastValue(repository.stopwatchAppWidgetInfo)()
+
+ Mockito.verify(appWidgetHost).stopListening()
+ }
+
+ private fun initCommunalWidgetRepository(): CommunalWidgetRepositoryImpl {
+ return CommunalWidgetRepositoryImpl(
+ appWidgetManager,
+ appWidgetHost,
+ broadcastDispatcher,
+ packageManager,
+ userManager,
+ userTracker,
+ logBuffer,
+ featureFlags,
+ )
+ }
+
+ private fun verifyBroadcastReceiverRegistered() {
+ Mockito.verify(broadcastDispatcher)
+ .registerReceiver(
+ any(),
+ any(),
+ nullable(),
+ nullable(),
+ anyInt(),
+ nullable(),
+ )
+ }
+
+ private fun verifyBroadcastReceiverNeverRegistered() {
+ Mockito.verify(broadcastDispatcher, Mockito.never())
+ .registerReceiver(
+ any(),
+ any(),
+ nullable(),
+ nullable(),
+ anyInt(),
+ nullable(),
+ )
+ }
+
+ private fun broadcastReceiverUpdate(): BroadcastReceiver {
+ val broadcastReceiverCaptor = kotlinArgumentCaptor<BroadcastReceiver>()
+ Mockito.verify(broadcastDispatcher)
+ .registerReceiver(
+ broadcastReceiverCaptor.capture(),
+ any(),
+ nullable(),
+ nullable(),
+ anyInt(),
+ nullable(),
+ )
+ broadcastReceiverCaptor.value.onReceive(null, null)
+ return broadcastReceiverCaptor.value
+ }
+
+ private fun featureFlagEnabled(enabled: Boolean) {
+ whenever(featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)).thenReturn(enabled)
+ }
+
+ private fun userUnlocked(userUnlocked: Boolean) {
+ whenever(userManager.isUserUnlockingOrUnlocked(userHandle)).thenReturn(userUnlocked)
+ }
+
+ private fun installedProviders(providers: List<AppWidgetProviderInfo>) {
+ whenever(appWidgetManager.installedProviders).thenReturn(providers)
+ }
+}
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
new file mode 100644
index 000000000000..d28f530f04f3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
+import com.android.systemui.communal.shared.CommunalAppWidgetInfo
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+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.MockitoAnnotations
+
+@SmallTest
+@RoboPilotTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class CommunalInteractorTest : SysuiTestCase() {
+ @Mock private lateinit var stopwatchAppWidgetInfo: CommunalAppWidgetInfo
+
+ private lateinit var testScope: TestScope
+
+ private lateinit var widgetRepository: FakeCommunalWidgetRepository
+ private lateinit var interactor: CommunalInteractor
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ testScope = TestScope()
+ widgetRepository = FakeCommunalWidgetRepository()
+ interactor = CommunalInteractor(widgetRepository)
+ }
+
+ @Test
+ fun testAppWidgetInfoFlow() =
+ testScope.runTest {
+ val lastAppWidgetInfo = collectLastValue(interactor.appWidgetInfo)
+ runCurrent()
+ assertThat(lastAppWidgetInfo()).isNull()
+
+ widgetRepository.setStopwatchAppWidgetInfo(stopwatchAppWidgetInfo)
+ runCurrent()
+ assertThat(lastAppWidgetInfo()).isEqualTo(stopwatchAppWidgetInfo)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprintTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprintTest.kt
index 783bb47bb9b0..e3a75f161318 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprintTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/ui/view/layout/blueprints/DefaultCommunalBlueprintTest.kt
@@ -5,25 +5,32 @@ import android.testing.TestableLooper
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.ui.view.layout.sections.DefaultCommunalWidgetSection
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
class DefaultCommunalBlueprintTest : SysuiTestCase() {
+ @Mock private lateinit var widgetSection: DefaultCommunalWidgetSection
+
private lateinit var blueprint: DefaultCommunalBlueprint
@Before
fun setup() {
- blueprint = DefaultCommunalBlueprint()
+ MockitoAnnotations.initMocks(this)
+ blueprint = DefaultCommunalBlueprint(widgetSection)
}
@Test
- fun apply_doesNothing() {
+ fun apply() {
val cs = ConstraintSet()
blueprint.apply(cs)
- // Nothing happens yet.
+ verify(widgetSection).apply(cs)
}
-} \ No newline at end of file
+}
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
new file mode 100644
index 000000000000..1a8c5830e453
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -0,0 +1,15 @@
+package com.android.systemui.communal.data.repository
+
+import com.android.systemui.communal.shared.CommunalAppWidgetInfo
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Fake implementation of [CommunalWidgetRepository] */
+class FakeCommunalWidgetRepository : CommunalWidgetRepository {
+ private val _stopwatchAppWidgetInfo = MutableStateFlow<CommunalAppWidgetInfo?>(null)
+ override val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> = _stopwatchAppWidgetInfo
+
+ fun setStopwatchAppWidgetInfo(appWidgetInfo: CommunalAppWidgetInfo) {
+ _stopwatchAppWidgetInfo.value = appWidgetInfo
+ }
+}