/* * Copyright (C) 2022 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.dreams.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.graphics.Color import android.util.Log import android.view.View import android.view.ViewGroup 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_DREAM import com.android.systemui.smartspace.SmartspacePrecondition import com.android.systemui.smartspace.SmartspaceTargetFilter 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.SmartspaceModule.Companion.DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN import com.android.systemui.smartspace.dagger.SmartspaceViewComponent 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 DreamSmartspaceController @Inject constructor( private val context: Context, private val smartspaceManager: SmartspaceManager, private val execution: Execution, @Main private val uiExecutor: Executor, private val smartspaceViewComponentFactory: SmartspaceViewComponent.Factory, @Named(DREAM_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition, @Named(DREAM_SMARTSPACE_TARGET_FILTER) private val optionalTargetFilter: Optional, @Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional, @Named(DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN) optionalWeatherPlugin: Optional, ) { 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) // A shadow copy of listeners is maintained to track whether the session should remain open. private var listeners = mutableSetOf() private var unfilteredListeners = mutableSetOf() // Smartspace can be used on multiple displays, such as when the user casts their screen private var smartspaceViews = mutableSetOf() 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) } var stateChangeListener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { val view = v as SmartspaceView // Until there is dream color matching view.setPrimaryTextColor(Color.WHITE) smartspaceViews.add(view) connectSession() view.setDozeAmount(0f) } override fun onViewDetachedFromWindow(v: View) { smartspaceViews.remove(v as SmartspaceView) if (smartspaceViews.isEmpty()) { disconnect() } } } 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, smartspaceDataPlugin, customView) connectSession() return view } 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 } return view } else { null } } private fun hasActiveSessionListeners(): Boolean { return smartspaceViews.isNotEmpty() || listeners.isNotEmpty() || unfilteredListeners.isNotEmpty() } private fun connectSession() { if (plugin == null && weatherPlugin == null) { return } if (session != null || !hasActiveSessionListeners()) { return } if (!precondition.conditionsMet()) { return } val newSession = smartspaceManager.createSmartspaceSession( SmartspaceConfig.Builder(context, UI_SURFACE_DREAM).build() ) Log.d(TAG, "Starting smartspace session for dream") newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener) this.session = newSession weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) } 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 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() 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) { unfilteredListeners.forEach { it.onSmartspaceTargetsUpdated(targets) } } /** * Adds a listener for the raw, unfiltered list of smartspace targets. This should be used * carefully, as it doesn't filter out targets which the user may not want shown. */ fun addUnfilteredListener(listener: SmartspaceTargetListener) { unfilteredListeners.add(listener) connectSession() } fun removeUnfilteredListener(listener: SmartspaceTargetListener) { unfilteredListeners.remove(listener) disconnect() } }