1 /*
<lambda>null2  * 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_WEATHER_SMARTSPACE_DATA_PLUGIN
38 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.LOCKSCREEN_SMARTSPACE_PRECONDITION
39 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.LOCKSCREEN_SMARTSPACE_TARGET_FILTER
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(LOCKSCREEN_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition,
58     @Named(LOCKSCREEN_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 (smartspaceManager == null) {
188             return
189         }
190         if (plugin == null && weatherPlugin == null) {
191             return
192         }
193         if (session != null || !hasActiveSessionListeners()) {
194             return
195         }
196 
197         if (!precondition.conditionsMet()) {
198             return
199         }
200 
201         val newSession = smartspaceManager.createSmartspaceSession(
202             SmartspaceConfig.Builder(context, UI_SURFACE_DREAM).build()
203         )
204         Log.d(TAG, "Starting smartspace session for dream")
205         newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener)
206         this.session = newSession
207 
208         weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
209         plugin?.registerSmartspaceEventNotifier {
210                 e ->
211             session?.notifySmartspaceEvent(e)
212         }
213 
214         reloadSmartspace()
215     }
216 
217     /**
218      * Disconnects the smartspace view from the smartspace service and cleans up any resources.
219      */
220     private fun disconnect() {
221         if (hasActiveSessionListeners()) return
222 
223         execution.assertIsMainThread()
224 
225         if (session == null) {
226             return
227         }
228 
229         session?.let {
230             it.removeOnTargetsAvailableListener(sessionListener)
231             it.close()
232         }
233 
234         session = null
235 
236         weatherPlugin?.registerSmartspaceEventNotifier(null)
237         weatherPlugin?.onTargetsAvailable(emptyList())
238 
239         plugin?.registerSmartspaceEventNotifier(null)
240         plugin?.onTargetsAvailable(emptyList())
241         Log.d(TAG, "Ending smartspace session for dream")
242     }
243 
244     fun addListener(listener: SmartspaceTargetListener) {
245         addAndRegisterListener(listener, plugin)
246     }
247 
248     fun removeListener(listener: SmartspaceTargetListener) {
249         removeAndUnregisterListener(listener, plugin)
250     }
251 
252     fun addListenerForWeatherPlugin(listener: SmartspaceTargetListener) {
253         addAndRegisterListener(listener, weatherPlugin)
254     }
255 
256     fun removeListenerForWeatherPlugin(listener: SmartspaceTargetListener) {
257         removeAndUnregisterListener(listener, weatherPlugin)
258     }
259 
260     private fun addAndRegisterListener(
261         listener: SmartspaceTargetListener,
262         smartspaceDataPlugin: BcSmartspaceDataPlugin?
263     ) {
264         execution.assertIsMainThread()
265         smartspaceDataPlugin?.registerListener(listener)
266         listeners.add(listener)
267 
268         connectSession()
269     }
270 
271     private fun removeAndUnregisterListener(
272         listener: SmartspaceTargetListener,
273         smartspaceDataPlugin: BcSmartspaceDataPlugin?
274     ) {
275         execution.assertIsMainThread()
276         smartspaceDataPlugin?.unregisterListener(listener)
277         listeners.remove(listener)
278         disconnect()
279     }
280 
281     private fun reloadSmartspace() {
282         session?.requestSmartspaceUpdate()
283     }
284 
285     private fun onTargetsAvailableUnfiltered(targets: List<SmartspaceTarget>) {
286         unfilteredListeners.forEach { it.onSmartspaceTargetsUpdated(targets) }
287     }
288 
289     /**
290      * Adds a listener for the raw, unfiltered list of smartspace targets. This should be used
291      * carefully, as it doesn't filter out targets which the user may not want shown.
292      */
293     fun addUnfilteredListener(listener: SmartspaceTargetListener) {
294         unfilteredListeners.add(listener)
295         connectSession()
296     }
297 
298     fun removeUnfilteredListener(listener: SmartspaceTargetListener) {
299         unfilteredListeners.remove(listener)
300         disconnect()
301     }
302 }
303