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

Commit e6a26619 authored by Coco Duan's avatar Coco Duan Committed by Automerger Merge Worker
Browse files

Merge "Connect weather complication view with DreamSmartspaceController" into...

Merge "Connect weather complication view with DreamSmartspaceController" into tm-qpr-dev am: 14075ced am: 317f08cf

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21732695



Change-Id: I4cea710634aeac3633ee97bdb13e7841a9ea62e5
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents 8963d739 317f08cf
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)
    }
}