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

Commit 0a7753c2 authored by Coco Duan's avatar Coco Duan
Browse files

Connect weather complication view with DreamSmartspaceController

- Added a weatherPlugin in DreamSmartspaceController to connect
  weather data provider and weather smartspace view.
- Added an optional parameter viewWithCustomLayout to create
  a SmartspaceView with custom layout other than the default
  view (weather.xml) returned from plugin.getView.

Bug: b/270170447
Test: atest DreamSmartspaceControllerTest
Test:  manually with ag/21737817
Change-Id: I2140e871416d1a4653f0e147106b0697ff6f30a1
parent e07cc86b
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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.
+68 −12
Original line number Diff line number Diff line
@@ -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()
    }
+6 −2
Original line number Diff line number Diff line
@@ -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)
+93 −5
Original line number Diff line number Diff line
@@ -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)
    }
}