diff options
| author | 2023-12-11 09:52:08 -0800 | |
|---|---|---|
| committer | 2023-12-12 10:06:01 -0800 | |
| commit | 8b96b1b8ddbcb7a45d0cd9baddb0234bde338469 (patch) | |
| tree | fa4d429aeea5c36bdd7481f3a4b6a3c5bd8d894c | |
| parent | 9c10535b71621c93d9b78924e4b320fce46e2b42 (diff) | |
Add CommunalSmartspaceController for UI_SURFACE_GLANCEABLE_HUB.
Flag: None
Bug: 314203588
Test: CommunalSmartspaceControllerTest
Change-Id: Idc564cbd942ca7188c9462081a1cf7687a528420
4 files changed, 377 insertions, 0 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt new file mode 100644 index 000000000000..ef2046d85a14 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt @@ -0,0 +1,172 @@ +/* + * 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.smartspace + +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.app.smartspace.SmartspaceTarget +import android.content.Context +import android.graphics.drawable.Drawable +import android.testing.TestableLooper +import android.view.View +import android.widget.FrameLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.smartspace.CommunalSmartspaceController +import com.android.systemui.plugins.BcSmartspaceConfigPlugin +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.util.concurrency.Execution +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.concurrent.Executor +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.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +class CommunalSmartspaceControllerTest : SysuiTestCase() { + @Mock private lateinit var smartspaceManager: SmartspaceManager + + @Mock private lateinit var execution: Execution + + @Mock private lateinit var uiExecutor: Executor + + @Mock private lateinit var targetFilter: SmartspaceTargetFilter + + @Mock private lateinit var plugin: BcSmartspaceDataPlugin + + @Mock private lateinit var precondition: SmartspacePrecondition + + @Mock private lateinit var listener: BcSmartspaceDataPlugin.SmartspaceTargetListener + + @Mock private lateinit var session: SmartspaceSession + + private lateinit var controller: CommunalSmartspaceController + + // TODO(b/272811280): Remove usage of real view + private val fakeParent = FrameLayout(context) + + /** + * A class which implements SmartspaceView and extends View. This is mocked to provide the right + * object inheritance and interface implementation used in CommunalSmartspaceController + */ + private class TestView(context: Context?) : View(context), SmartspaceView { + override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {} + + override fun registerConfigProvider(plugin: BcSmartspaceConfigPlugin?) {} + + override fun setPrimaryTextColor(color: Int) {} + + override fun setUiSurface(uiSurface: String) {} + + override fun setDozeAmount(amount: Float) {} + + override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {} + + override fun setFalsingManager(falsingManager: FalsingManager?) {} + + override fun setDnd(image: Drawable?, description: String?) {} + + override fun setNextAlarm(image: Drawable?, description: String?) {} + + override fun setMediaTarget(target: SmartspaceTarget?) {} + + override fun getSelectedPage(): Int { + return 0 + } + + override fun getCurrentCardTopPadding(): Int { + return 0 + } + } + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + `when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(session) + + controller = + CommunalSmartspaceController( + context, + smartspaceManager, + execution, + uiExecutor, + precondition, + Optional.of(targetFilter), + Optional.of(plugin) + ) + } + + /** Ensures smartspace session begins on a listener only flow. */ + @Test + fun testConnectOnListen() { + `when`(precondition.conditionsMet()).thenReturn(true) + controller.addListener(listener) + + verify(smartspaceManager).createSmartspaceSession(any()) + + var targetListener = + withArgCaptor<SmartspaceSession.OnTargetsAvailableListener> { + verify(session).addOnTargetsAvailableListener(any(), capture()) + } + + `when`(targetFilter.filterSmartspaceTarget(any())).thenReturn(true) + + var target = Mockito.mock(SmartspaceTarget::class.java) + targetListener.onTargetsAvailable(listOf(target)) + + var targets = + withArgCaptor<List<SmartspaceTarget>> { verify(plugin).onTargetsAvailable(capture()) } + + assertThat(targets.contains(target)).isTrue() + + controller.removeListener(listener) + + verify(session).close() + } + + /** + * Ensures session is closed and weather plugin unregisters the notifier when weather smartspace + * view is detached. + */ + @Test + fun testDisconnect_emitsEmptyListAndRemovesNotifier() { + `when`(precondition.conditionsMet()).thenReturn(true) + controller.addListener(listener) + + verify(smartspaceManager).createSmartspaceSession(any()) + + controller.removeListener(listener) + + verify(session).close() + + // And the listener receives an empty list of targets and unregisters the notifier + verify(plugin).onTargetsAvailable(emptyList()) + verify(plugin).registerSmartspaceEventNotifier(null) + } +} diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java index 64c0f99f4ba7..c99cb39f91bf 100644 --- a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java +++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java @@ -44,6 +44,7 @@ public interface BcSmartspaceDataPlugin extends Plugin { String UI_SURFACE_HOME_SCREEN = "home"; String UI_SURFACE_MEDIA = "media_data_manager"; String UI_SURFACE_DREAM = "dream"; + String UI_SURFACE_GLANCEABLE_HUB = "glanceable_hub"; String ACTION = "com.android.systemui.action.PLUGIN_BC_SMARTSPACE_DATA"; int VERSION = 1; diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt new file mode 100644 index 000000000000..c5610c877f57 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt @@ -0,0 +1,195 @@ +/* + * 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.smartspace + +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.app.smartspace.SmartspaceTarget +import android.content.Context +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView +import com.android.systemui.plugins.BcSmartspaceDataPlugin.UI_SURFACE_GLANCEABLE_HUB +import com.android.systemui.smartspace.SmartspacePrecondition +import com.android.systemui.smartspace.SmartspaceTargetFilter +import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_PRECONDITION +import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_TARGET_FILTER +import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN +import com.android.systemui.util.concurrency.Execution +import java.util.Optional +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Named + +/** Controller for managing the smartspace view on the dream */ +@SysUISingleton +class CommunalSmartspaceController +@Inject +constructor( + private val context: Context, + private val smartspaceManager: SmartspaceManager?, + private val execution: Execution, + @Main private val uiExecutor: Executor, + @Named(DREAM_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition, + @Named(DREAM_SMARTSPACE_TARGET_FILTER) + private val optionalTargetFilter: Optional<SmartspaceTargetFilter>, + @Named(GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>, +) { + companion object { + private const val TAG = "CommunalSmartspaceCtrlr" + } + + private var session: SmartspaceSession? = null + private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null) + private var targetFilter: SmartspaceTargetFilter? = optionalTargetFilter.orElse(null) + + // A shadow copy of listeners is maintained to track whether the session should remain open. + private var listeners = mutableSetOf<SmartspaceTargetListener>() + + private var unfilteredListeners = mutableSetOf<SmartspaceTargetListener>() + + // Smartspace can be used on multiple displays, such as when the user casts their screen + private var smartspaceViews = mutableSetOf<SmartspaceView>() + + var preconditionListener = + object : SmartspacePrecondition.Listener { + override fun onCriteriaChanged() { + reloadSmartspace() + } + } + + init { + precondition.addListener(preconditionListener) + } + + var filterListener = + object : SmartspaceTargetFilter.Listener { + override fun onCriteriaChanged() { + reloadSmartspace() + } + } + + init { + targetFilter?.addListener(filterListener) + } + + private val sessionListener = + SmartspaceSession.OnTargetsAvailableListener { targets -> + execution.assertIsMainThread() + + val filteredTargets = + targets.filter { targetFilter?.filterSmartspaceTarget(it) ?: true } + plugin?.onTargetsAvailable(filteredTargets) + } + + private fun hasActiveSessionListeners(): Boolean { + return smartspaceViews.isNotEmpty() || + listeners.isNotEmpty() || + unfilteredListeners.isNotEmpty() + } + + private fun connectSession() { + if (smartspaceManager == null) { + return + } + if (plugin == null) { + return + } + if (session != null || !hasActiveSessionListeners()) { + return + } + + if (!precondition.conditionsMet()) { + return + } + + val newSession = + smartspaceManager.createSmartspaceSession( + SmartspaceConfig.Builder(context, UI_SURFACE_GLANCEABLE_HUB).build() + ) + Log.d(TAG, "Starting smartspace session for dream") + newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener) + this.session = newSession + + plugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) } + + reloadSmartspace() + } + + /** Disconnects the smartspace view from the smartspace service and cleans up any resources. */ + private fun disconnect() { + if (hasActiveSessionListeners()) return + + execution.assertIsMainThread() + + if (session == null) { + return + } + + session?.let { + it.removeOnTargetsAvailableListener(sessionListener) + it.close() + } + + session = null + + plugin?.registerSmartspaceEventNotifier(null) + plugin?.onTargetsAvailable(emptyList()) + Log.d(TAG, "Ending smartspace session for dream") + } + + fun addListener(listener: SmartspaceTargetListener) { + addAndRegisterListener(listener, plugin) + } + + fun removeListener(listener: SmartspaceTargetListener) { + removeAndUnregisterListener(listener, plugin) + } + + private fun addAndRegisterListener( + listener: SmartspaceTargetListener, + smartspaceDataPlugin: BcSmartspaceDataPlugin? + ) { + execution.assertIsMainThread() + smartspaceDataPlugin?.registerListener(listener) + listeners.add(listener) + + connectSession() + } + + private fun removeAndUnregisterListener( + listener: SmartspaceTargetListener, + smartspaceDataPlugin: BcSmartspaceDataPlugin? + ) { + execution.assertIsMainThread() + smartspaceDataPlugin?.unregisterListener(listener) + listeners.remove(listener) + disconnect() + } + + private fun reloadSmartspace() { + session?.requestSmartspaceUpdate() + } + + private fun onTargetsAvailableUnfiltered(targets: List<SmartspaceTarget>) { + unfilteredListeners.forEach { it.onSmartspaceTargetsUpdated(targets) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt index c59ef2632f15..d26fded19cc1 100644 --- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt @@ -59,6 +59,11 @@ abstract class SmartspaceModule { * The BcSmartspaceDataPlugin for the standalone weather. */ const val WEATHER_SMARTSPACE_DATA_PLUGIN = "weather_smartspace_data_plugin" + + /** + * The BcSmartspaceDataProvider for the glanceable hub. + */ + const val GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN = "glanceable_hub_smartspace_data_plugin" } @BindsOptionalOf @@ -78,4 +83,8 @@ abstract class SmartspaceModule { abstract fun bindSmartspacePrecondition( lockscreenPrecondition: LockscreenPrecondition? ): SmartspacePrecondition? + + @BindsOptionalOf + @Named(GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN) + abstract fun optionalBcSmartspaceDataPlugin(): BcSmartspaceDataPlugin? } |