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

Commit 8b96b1b8 authored by Xiaowen Lei's avatar Xiaowen Lei
Browse files

Add CommunalSmartspaceController for UI_SURFACE_GLANCEABLE_HUB.

Flag: None
Bug: 314203588
Test: CommunalSmartspaceControllerTest
Change-Id: Idc564cbd942ca7188c9462081a1cf7687a528420
parent 9c10535b
Loading
Loading
Loading
Loading
+172 −0
Original line number Original line Diff line number Diff line
/*
 * 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)
    }
}
+1 −0
Original line number Original line Diff line number Diff line
@@ -44,6 +44,7 @@ public interface BcSmartspaceDataPlugin extends Plugin {
    String UI_SURFACE_HOME_SCREEN = "home";
    String UI_SURFACE_HOME_SCREEN = "home";
    String UI_SURFACE_MEDIA = "media_data_manager";
    String UI_SURFACE_MEDIA = "media_data_manager";
    String UI_SURFACE_DREAM = "dream";
    String UI_SURFACE_DREAM = "dream";
    String UI_SURFACE_GLANCEABLE_HUB = "glanceable_hub";


    String ACTION = "com.android.systemui.action.PLUGIN_BC_SMARTSPACE_DATA";
    String ACTION = "com.android.systemui.action.PLUGIN_BC_SMARTSPACE_DATA";
    int VERSION = 1;
    int VERSION = 1;
+195 −0
Original line number Original line Diff line number Diff line
/*
 * 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) }
    }
}
+9 −0
Original line number Original line Diff line number Diff line
@@ -59,6 +59,11 @@ abstract class SmartspaceModule {
         * The BcSmartspaceDataPlugin for the standalone weather.
         * The BcSmartspaceDataPlugin for the standalone weather.
         */
         */
        const val WEATHER_SMARTSPACE_DATA_PLUGIN = "weather_smartspace_data_plugin"
        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
    @BindsOptionalOf
@@ -78,4 +83,8 @@ abstract class SmartspaceModule {
    abstract fun bindSmartspacePrecondition(
    abstract fun bindSmartspacePrecondition(
        lockscreenPrecondition: LockscreenPrecondition?
        lockscreenPrecondition: LockscreenPrecondition?
    ): SmartspacePrecondition?
    ): SmartspacePrecondition?

    @BindsOptionalOf
    @Named(GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN)
    abstract fun optionalBcSmartspaceDataPlugin(): BcSmartspaceDataPlugin?
}
}