diff options
4 files changed, 168 insertions, 19 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java index 9b954f5f0c4a..6808142e98d2 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java @@ -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. diff --git a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt index 63f63a5093d2..78e132ff6397 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt @@ -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() } diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt index 5736a5cdc05a..26149321946d 100644 --- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt @@ -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) diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt index a280510009a7..58b44ae5bcbd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt @@ -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 @@ -69,12 +71,21 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { 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 @Spy @@ -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) + } } |