1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.dreams.smartspace
18 
19 import android.app.smartspace.SmartspaceConfig
20 import android.app.smartspace.SmartspaceManager
21 import android.app.smartspace.SmartspaceSession
22 import android.app.smartspace.SmartspaceTarget
23 import android.content.Context
24 import android.graphics.Color
25 import android.util.Log
26 import android.view.View
27 import android.view.ViewGroup
28 import com.android.systemui.dagger.SysUISingleton
29 import com.android.systemui.dagger.qualifiers.Main
30 import com.android.systemui.plugins.BcSmartspaceDataPlugin
31 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
32 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
33 import com.android.systemui.plugins.BcSmartspaceDataPlugin.UI_SURFACE_DREAM
34 import com.android.systemui.smartspace.SmartspacePrecondition
35 import com.android.systemui.smartspace.SmartspaceTargetFilter
36 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_DATA_PLUGIN
37 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_PRECONDITION
38 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_TARGET_FILTER
39 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN
40 import com.android.systemui.smartspace.dagger.SmartspaceViewComponent
41 import com.android.systemui.util.concurrency.Execution
42 import java.util.Optional
43 import java.util.concurrent.Executor
44 import javax.inject.Inject
45 import javax.inject.Named
46 
47 /**
48  * Controller for managing the smartspace view on the dream
49  */
50 @SysUISingleton
51 class DreamSmartspaceController @Inject constructor(
52     private val context: Context,
53     private val smartspaceManager: SmartspaceManager,
54     private val execution: Execution,
55     @Main private val uiExecutor: Executor,
56     private val smartspaceViewComponentFactory: SmartspaceViewComponent.Factory,
57     @Named(DREAM_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition,
58     @Named(DREAM_SMARTSPACE_TARGET_FILTER)
59     private val optionalTargetFilter: Optional<SmartspaceTargetFilter>,
60     @Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>,
61     @Named(DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN)
62     optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>,
63 ) {
64     companion object {
65         private const val TAG = "DreamSmartspaceCtrlr"
66     }
67 
68     private var session: SmartspaceSession? = null
69     private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null)
70     private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
71     private var targetFilter: SmartspaceTargetFilter? = optionalTargetFilter.orElse(null)
72 
73     // A shadow copy of listeners is maintained to track whether the session should remain open.
74     private var listeners = mutableSetOf<SmartspaceTargetListener>()
75 
76     private var unfilteredListeners = mutableSetOf<SmartspaceTargetListener>()
77 
78     // Smartspace can be used on multiple displays, such as when the user casts their screen
79     private var smartspaceViews = mutableSetOf<SmartspaceView>()
80 
81     var preconditionListener = object : SmartspacePrecondition.Listener {
82         override fun onCriteriaChanged() {
83             reloadSmartspace()
84         }
85     }
86 
87     init {
88         precondition.addListener(preconditionListener)
89     }
90 
91     var filterListener = object : SmartspaceTargetFilter.Listener {
92         override fun onCriteriaChanged() {
93             reloadSmartspace()
94         }
95     }
96 
97     init {
98         targetFilter?.addListener(filterListener)
99     }
100 
101     var stateChangeListener = object : View.OnAttachStateChangeListener {
102         override fun onViewAttachedToWindow(v: View) {
103             val view = v as SmartspaceView
104             // Until there is dream color matching
105             view.setPrimaryTextColor(Color.WHITE)
106             smartspaceViews.add(view)
107             connectSession()
108             view.setDozeAmount(0f)
109         }
110 
111         override fun onViewDetachedFromWindow(v: View) {
112             smartspaceViews.remove(v as SmartspaceView)
113 
114             if (smartspaceViews.isEmpty()) {
115                 disconnect()
116             }
117         }
118     }
119 
120     private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets ->
121         execution.assertIsMainThread()
122 
123         // The weather data plugin takes unfiltered targets and performs the filtering internally.
124         weatherPlugin?.onTargetsAvailable(targets)
125 
126         onTargetsAvailableUnfiltered(targets)
127         val filteredTargets = targets.filter { targetFilter?.filterSmartspaceTarget(it) ?: true }
128         plugin?.onTargetsAvailable(filteredTargets)
129     }
130 
131     /**
132      * Constructs the weather view with custom layout and connects it to the weather plugin.
133      */
134     fun buildAndConnectWeatherView(parent: ViewGroup, customView: View?): View? {
135         return buildAndConnectViewWithPlugin(parent, weatherPlugin, customView)
136     }
137 
138     /**
139      * Constructs the smartspace view and connects it to the smartspace service.
140      */
141     fun buildAndConnectView(parent: ViewGroup): View? {
142         return buildAndConnectViewWithPlugin(parent, plugin, null)
143     }
144 
145     private fun buildAndConnectViewWithPlugin(
146         parent: ViewGroup,
147         smartspaceDataPlugin: BcSmartspaceDataPlugin?,
148         customView: View?
149     ): View? {
150         execution.assertIsMainThread()
151 
152         if (!precondition.conditionsMet()) {
153             throw RuntimeException("Cannot build view when not enabled")
154         }
155 
156         val view = buildView(parent, smartspaceDataPlugin, customView)
157 
158         connectSession()
159 
160         return view
161     }
162 
163     private fun buildView(
164         parent: ViewGroup,
165         smartspaceDataPlugin: BcSmartspaceDataPlugin?,
166         customView: View?
167     ): View? {
168         return if (smartspaceDataPlugin != null) {
169             val view = smartspaceViewComponentFactory.create(parent, smartspaceDataPlugin,
170                 stateChangeListener, customView)
171                 .getView()
172             if (view !is View) {
173                 return null
174             }
175             return view
176         } else {
177             null
178         }
179     }
180 
181     private fun hasActiveSessionListeners(): Boolean {
182         return smartspaceViews.isNotEmpty() || listeners.isNotEmpty() ||
183             unfilteredListeners.isNotEmpty()
184     }
185 
186     private fun connectSession() {
187         if (plugin == null && weatherPlugin == null) {
188             return
189         }
190         if (session != null || !hasActiveSessionListeners()) {
191             return
192         }
193 
194         if (!precondition.conditionsMet()) {
195             return
196         }
197 
198         val newSession = smartspaceManager.createSmartspaceSession(
199             SmartspaceConfig.Builder(context, UI_SURFACE_DREAM).build()
200         )
201         Log.d(TAG, "Starting smartspace session for dream")
202         newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener)
203         this.session = newSession
204 
205         weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
206         plugin?.registerSmartspaceEventNotifier {
207                 e ->
208             session?.notifySmartspaceEvent(e)
209         }
210 
211         reloadSmartspace()
212     }
213 
214     /**
215      * Disconnects the smartspace view from the smartspace service and cleans up any resources.
216      */
217     private fun disconnect() {
218         if (hasActiveSessionListeners()) return
219 
220         execution.assertIsMainThread()
221 
222         if (session == null) {
223             return
224         }
225 
226         session?.let {
227             it.removeOnTargetsAvailableListener(sessionListener)
228             it.close()
229         }
230 
231         session = null
232 
233         weatherPlugin?.registerSmartspaceEventNotifier(null)
234         weatherPlugin?.onTargetsAvailable(emptyList())
235 
236         plugin?.registerSmartspaceEventNotifier(null)
237         plugin?.onTargetsAvailable(emptyList())
238         Log.d(TAG, "Ending smartspace session for dream")
239     }
240 
241     fun addListener(listener: SmartspaceTargetListener) {
242         addAndRegisterListener(listener, plugin)
243     }
244 
245     fun removeListener(listener: SmartspaceTargetListener) {
246         removeAndUnregisterListener(listener, plugin)
247     }
248 
249     fun addListenerForWeatherPlugin(listener: SmartspaceTargetListener) {
250         addAndRegisterListener(listener, weatherPlugin)
251     }
252 
253     fun removeListenerForWeatherPlugin(listener: SmartspaceTargetListener) {
254         removeAndUnregisterListener(listener, weatherPlugin)
255     }
256 
257     private fun addAndRegisterListener(
258         listener: SmartspaceTargetListener,
259         smartspaceDataPlugin: BcSmartspaceDataPlugin?
260     ) {
261         execution.assertIsMainThread()
262         smartspaceDataPlugin?.registerListener(listener)
263         listeners.add(listener)
264 
265         connectSession()
266     }
267 
268     private fun removeAndUnregisterListener(
269         listener: SmartspaceTargetListener,
270         smartspaceDataPlugin: BcSmartspaceDataPlugin?
271     ) {
272         execution.assertIsMainThread()
273         smartspaceDataPlugin?.unregisterListener(listener)
274         listeners.remove(listener)
275         disconnect()
276     }
277 
278     private fun reloadSmartspace() {
279         session?.requestSmartspaceUpdate()
280     }
281 
282     private fun onTargetsAvailableUnfiltered(targets: List<SmartspaceTarget>) {
283         unfilteredListeners.forEach { it.onSmartspaceTargetsUpdated(targets) }
284     }
285 
286     /**
287      * Adds a listener for the raw, unfiltered list of smartspace targets. This should be used
288      * carefully, as it doesn't filter out targets which the user may not want shown.
289      */
290     fun addUnfilteredListener(listener: SmartspaceTargetListener) {
291         unfilteredListeners.add(listener)
292         connectSession()
293     }
294 
295     fun removeUnfilteredListener(listener: SmartspaceTargetListener) {
296         unfilteredListeners.remove(listener)
297         disconnect()
298     }
299 }
300