Merge "Connect weather complication view with DreamSmartspaceController" into tm-qpr-dev

This commit is contained in:
Coco Duan
2023-03-14 06:40:22 +00:00
committed by Android (Google) Code Review
4 changed files with 168 additions and 19 deletions

View File

@@ -51,6 +51,7 @@ public interface RegisteredComplicationsModule {
int DREAM_MEDIA_COMPLICATION_WEIGHT = 0;
int DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT = 4;
int DREAM_MEDIA_ENTRY_COMPLICATION_WEIGHT = 3;
int DREAM_WEATHER_COMPLICATION_WEIGHT = 0;
/**
* Provides layout parameters for the clock time complication.

View File

@@ -30,14 +30,15 @@ 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_DREAM
import com.android.systemui.smartspace.SmartspacePrecondition
import com.android.systemui.smartspace.SmartspaceTargetFilter
import com.android.systemui.smartspace.dagger.SmartspaceModule
import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_DATA_PLUGIN
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.SmartspaceViewComponent
import com.android.systemui.util.concurrency.Execution
import java.lang.RuntimeException
import java.util.Optional
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -56,13 +57,16 @@ class DreamSmartspaceController @Inject constructor(
@Named(DREAM_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition,
@Named(DREAM_SMARTSPACE_TARGET_FILTER)
private val optionalTargetFilter: Optional<SmartspaceTargetFilter>,
@Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>
@Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>,
@Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN)
optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>,
) {
companion object {
private const val TAG = "DreamSmartspaceCtrlr"
}
private var session: SmartspaceSession? = null
private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null)
private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
private var targetFilter: SmartspaceTargetFilter? = optionalTargetFilter.orElse(null)
@@ -116,31 +120,54 @@ class DreamSmartspaceController @Inject constructor(
private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets ->
execution.assertIsMainThread()
// The weather data plugin takes unfiltered targets and performs the filtering internally.
weatherPlugin?.onTargetsAvailable(targets)
onTargetsAvailableUnfiltered(targets)
val filteredTargets = targets.filter { targetFilter?.filterSmartspaceTarget(it) ?: true }
plugin?.onTargetsAvailable(filteredTargets)
}
/**
* Constructs the weather view with custom layout and connects it to the weather plugin.
*/
fun buildAndConnectWeatherView(parent: ViewGroup, customView: View?): View? {
return buildAndConnectViewWithPlugin(parent, weatherPlugin, customView)
}
/**
* Constructs the smartspace view and connects it to the smartspace service.
*/
fun buildAndConnectView(parent: ViewGroup): View? {
return buildAndConnectViewWithPlugin(parent, plugin, null)
}
private fun buildAndConnectViewWithPlugin(
parent: ViewGroup,
smartspaceDataPlugin: BcSmartspaceDataPlugin?,
customView: View?
): View? {
execution.assertIsMainThread()
if (!precondition.conditionsMet()) {
throw RuntimeException("Cannot build view when not enabled")
}
val view = buildView(parent)
val view = buildView(parent, smartspaceDataPlugin, customView)
connectSession()
return view
}
private fun buildView(parent: ViewGroup): View? {
return if (plugin != null) {
var view = smartspaceViewComponentFactory.create(parent, plugin, stateChangeListener)
private fun buildView(
parent: ViewGroup,
smartspaceDataPlugin: BcSmartspaceDataPlugin?,
customView: View?
): View? {
return if (smartspaceDataPlugin != null) {
val view = smartspaceViewComponentFactory.create(parent, smartspaceDataPlugin,
stateChangeListener, customView)
.getView()
if (view !is View) {
return null
@@ -157,7 +184,10 @@ class DreamSmartspaceController @Inject constructor(
}
private fun connectSession() {
if (plugin == null || session != null || !hasActiveSessionListeners()) {
if (plugin == null && weatherPlugin == null) {
return
}
if (session != null || !hasActiveSessionListeners()) {
return
}
@@ -166,13 +196,14 @@ class DreamSmartspaceController @Inject constructor(
}
val newSession = smartspaceManager.createSmartspaceSession(
SmartspaceConfig.Builder(context, "dream").build()
SmartspaceConfig.Builder(context, UI_SURFACE_DREAM).build()
)
Log.d(TAG, "Starting smartspace session for dream")
newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener)
this.session = newSession
plugin.registerSmartspaceEventNotifier {
weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
plugin?.registerSmartspaceEventNotifier {
e ->
session?.notifySmartspaceEvent(e)
}
@@ -199,22 +230,47 @@ class DreamSmartspaceController @Inject constructor(
session = null
weatherPlugin?.registerSmartspaceEventNotifier(null)
weatherPlugin?.onTargetsAvailable(emptyList())
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)
}
fun addListenerForWeatherPlugin(listener: SmartspaceTargetListener) {
addAndRegisterListener(listener, weatherPlugin)
}
fun removeListenerForWeatherPlugin(listener: SmartspaceTargetListener) {
removeAndUnregisterListener(listener, weatherPlugin)
}
private fun addAndRegisterListener(
listener: SmartspaceTargetListener,
smartspaceDataPlugin: BcSmartspaceDataPlugin?
) {
execution.assertIsMainThread()
plugin?.registerListener(listener)
smartspaceDataPlugin?.registerListener(listener)
listeners.add(listener)
connectSession()
}
fun removeListener(listener: SmartspaceTargetListener) {
private fun removeAndUnregisterListener(
listener: SmartspaceTargetListener,
smartspaceDataPlugin: BcSmartspaceDataPlugin?
) {
execution.assertIsMainThread()
plugin?.unregisterListener(listener)
smartspaceDataPlugin?.unregisterListener(listener)
listeners.remove(listener)
disconnect()
}

View File

@@ -37,7 +37,8 @@ interface SmartspaceViewComponent {
fun create(
@BindsInstance parent: ViewGroup,
@BindsInstance @Named(PLUGIN) plugin: BcSmartspaceDataPlugin,
@BindsInstance onAttachListener: View.OnAttachStateChangeListener
@BindsInstance onAttachListener: View.OnAttachStateChangeListener,
@BindsInstance viewWithCustomLayout: View? = null
): SmartspaceViewComponent
}
@@ -53,10 +54,13 @@ interface SmartspaceViewComponent {
falsingManager: FalsingManager,
parent: ViewGroup,
@Named(PLUGIN) plugin: BcSmartspaceDataPlugin,
viewWithCustomLayout: View?,
onAttachListener: View.OnAttachStateChangeListener
):
BcSmartspaceDataPlugin.SmartspaceView {
val ssView = plugin.getView(parent)
val ssView = viewWithCustomLayout
as? BcSmartspaceDataPlugin.SmartspaceView
?: plugin.getView(parent)
// Currently, this is only used to provide SmartspaceView on Dream surface.
ssView.setUiSurface(UI_SURFACE_DREAM)
ssView.registerDataProvider(plugin)

View File

@@ -24,6 +24,7 @@ import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dreams.smartspace.DreamSmartspaceController
@@ -46,6 +47,7 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyInt
import org.mockito.MockitoAnnotations
import org.mockito.Spy
@@ -68,12 +70,21 @@ class DreamSmartspaceControllerTest : SysuiTestCase() {
@Mock
private lateinit var viewComponent: SmartspaceViewComponent
@Mock
private lateinit var weatherViewComponent: SmartspaceViewComponent
@Spy
private var weatherSmartspaceView: SmartspaceView = TestView(context)
@Mock
private lateinit var targetFilter: SmartspaceTargetFilter
@Mock
private lateinit var plugin: BcSmartspaceDataPlugin
@Mock
private lateinit var weatherPlugin: BcSmartspaceDataPlugin
@Mock
private lateinit var precondition: SmartspacePrecondition
@@ -88,6 +99,9 @@ class DreamSmartspaceControllerTest : SysuiTestCase() {
private lateinit var controller: DreamSmartspaceController
// 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 DreamSmartspaceController
@@ -121,13 +135,17 @@ class DreamSmartspaceControllerTest : SysuiTestCase() {
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
`when`(viewComponentFactory.create(any(), eq(plugin), any()))
`when`(viewComponentFactory.create(any(), eq(plugin), any(), eq(null)))
.thenReturn(viewComponent)
`when`(viewComponent.getView()).thenReturn(smartspaceView)
`when`(viewComponentFactory.create(any(), eq(weatherPlugin), any(), any()))
.thenReturn(weatherViewComponent)
`when`(weatherViewComponent.getView()).thenReturn(weatherSmartspaceView)
`when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(session)
controller = DreamSmartspaceController(context, smartspaceManager, execution, uiExecutor,
viewComponentFactory, precondition, Optional.of(targetFilter), Optional.of(plugin))
viewComponentFactory, precondition, Optional.of(targetFilter), Optional.of(plugin),
Optional.of(weatherPlugin))
}
/**
@@ -168,11 +186,11 @@ class DreamSmartspaceControllerTest : SysuiTestCase() {
`when`(precondition.conditionsMet()).thenReturn(true)
controller.buildAndConnectView(Mockito.mock(ViewGroup::class.java))
var stateChangeListener = withArgCaptor<View.OnAttachStateChangeListener> {
verify(viewComponentFactory).create(any(), eq(plugin), capture())
val stateChangeListener = withArgCaptor<View.OnAttachStateChangeListener> {
verify(viewComponentFactory).create(any(), eq(plugin), capture(), eq(null))
}
var mockView = Mockito.mock(TestView::class.java)
val mockView = Mockito.mock(TestView::class.java)
`when`(precondition.conditionsMet()).thenReturn(true)
stateChangeListener.onViewAttachedToWindow(mockView)
@@ -183,4 +201,74 @@ class DreamSmartspaceControllerTest : SysuiTestCase() {
verify(session).close()
}
/**
* Ensures session is created when weather smartspace view is created and attached.
*/
@Test
fun testConnectOnWeatherViewCreate() {
`when`(precondition.conditionsMet()).thenReturn(true)
val customView = Mockito.mock(TestView::class.java)
val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView)
val weatherSmartspaceView = weatherView as SmartspaceView
fakeParent.addView(weatherView)
// Then weather view is created with custom view and the default weatherPlugin.getView
// should not be called
verify(viewComponentFactory).create(eq(fakeParent), eq(weatherPlugin), any(),
eq(customView))
verify(weatherPlugin, Mockito.never()).getView(fakeParent)
// And then session is created
controller.stateChangeListener.onViewAttachedToWindow(weatherView)
verify(smartspaceManager).createSmartspaceSession(any())
verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
verify(weatherSmartspaceView).setDozeAmount(0f)
}
/**
* Ensures weather plugin registers target listener when it is added from the controller.
*/
@Test
fun testAddListenerInController_registersListenerForWeatherPlugin() {
val customView = Mockito.mock(TestView::class.java)
`when`(precondition.conditionsMet()).thenReturn(true)
// Given a session is created
val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView)
controller.stateChangeListener.onViewAttachedToWindow(weatherView)
verify(smartspaceManager).createSmartspaceSession(any())
// When a listener is added
controller.addListenerForWeatherPlugin(listener)
// Then the listener is registered to the weather plugin only
verify(weatherPlugin).registerListener(listener)
verify(plugin, Mockito.never()).registerListener(any())
}
/**
* 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)
// Given a session is created
val customView = Mockito.mock(TestView::class.java)
val weatherView = controller.buildAndConnectWeatherView(fakeParent, customView)
controller.stateChangeListener.onViewAttachedToWindow(weatherView)
verify(smartspaceManager).createSmartspaceSession(any())
// When view is detached
controller.stateChangeListener.onViewDetachedFromWindow(weatherView)
// Then the session is closed
verify(session).close()
// And the listener receives an empty list of targets and unregisters the notifier
verify(weatherPlugin).onTargetsAvailable(emptyList())
verify(weatherPlugin).registerSmartspaceEventNotifier(null)
}
}