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.graphics.Color
24 import android.util.Log
25 import android.view.View
26 import android.view.ViewGroup
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.dagger.qualifiers.Main
29 import com.android.systemui.plugins.BcSmartspaceDataPlugin
30 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
31 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
32 import com.android.systemui.plugins.BcSmartspaceDataPlugin.UI_SURFACE_DREAM
33 import com.android.systemui.settings.UserTracker
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 /** Controller for managing the smartspace view on the dream */
48 @SysUISingleton
49 class DreamSmartspaceController
50 @Inject
51 constructor(
52     private val userTracker: UserTracker,
53     private val execution: Execution,
54     @Main private val uiExecutor: Executor,
55     private val smartspaceViewComponentFactory: SmartspaceViewComponent.Factory,
56     @Named(LOCKSCREEN_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition,
57     @Named(LOCKSCREEN_SMARTSPACE_TARGET_FILTER)
58     private val optionalTargetFilter: Optional<SmartspaceTargetFilter>,
59     @Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>,
60     @Named(DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN)
61     optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>,
62 ) {
63     companion object {
64         private const val TAG = "DreamSmartspaceCtrlr"
65     }
66 
67     private var userSmartspaceManager: SmartspaceManager? = null
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 =
82         object : SmartspacePrecondition.Listener {
83             override fun onCriteriaChanged() {
84                 reloadSmartspace()
85             }
86         }
87 
88     init {
89         precondition.addListener(preconditionListener)
90     }
91 
92     var filterListener =
93         object : SmartspaceTargetFilter.Listener {
94             override fun onCriteriaChanged() {
95                 reloadSmartspace()
96             }
97         }
98 
99     init {
100         targetFilter?.addListener(filterListener)
101     }
102 
103     var stateChangeListener =
104         object : View.OnAttachStateChangeListener {
105             override fun onViewAttachedToWindow(v: View) {
106                 val view = v as SmartspaceView
107                 // Until there is dream color matching
108                 view.setPrimaryTextColor(Color.WHITE)
109                 smartspaceViews.add(view)
110                 connectSession()
111                 view.setDozeAmount(0f)
112             }
113 
114             override fun onViewDetachedFromWindow(v: View) {
115                 smartspaceViews.remove(v as SmartspaceView)
116 
117                 if (smartspaceViews.isEmpty()) {
118                     disconnect()
119                 }
120             }
121         }
122 
123     private val sessionListener =
124         SmartspaceSession.OnTargetsAvailableListener { targets ->
125             execution.assertIsMainThread()
126 
127             // The weather data plugin takes unfiltered targets and performs the filtering
128             // internally.
129             weatherPlugin?.onTargetsAvailable(targets)
130 
131             onTargetsAvailableUnfiltered(targets)
132             val filteredTargets =
133                 targets.filter { targetFilter?.filterSmartspaceTarget(it) ?: true }
134             plugin?.onTargetsAvailable(filteredTargets)
135         }
136 
137     /** Constructs the weather view with custom layout and connects it to the weather plugin. */
138     fun buildAndConnectWeatherView(parent: ViewGroup, customView: View?): View? {
139         return buildAndConnectViewWithPlugin(parent, weatherPlugin, customView)
140     }
141 
142     /** Constructs the smartspace view and connects it to the smartspace service. */
143     fun buildAndConnectView(parent: ViewGroup): View? {
144         return buildAndConnectViewWithPlugin(parent, plugin, null)
145     }
146 
147     private fun buildAndConnectViewWithPlugin(
148         parent: ViewGroup,
149         smartspaceDataPlugin: BcSmartspaceDataPlugin?,
150         customView: View?,
151     ): View? {
152         execution.assertIsMainThread()
153 
154         if (!precondition.conditionsMet()) {
155             throw RuntimeException("Cannot build view when not enabled")
156         }
157 
158         val view = buildView(parent, smartspaceDataPlugin, customView)
159 
160         connectSession()
161 
162         return view
163     }
164 
165     private fun buildView(
166         parent: ViewGroup,
167         smartspaceDataPlugin: BcSmartspaceDataPlugin?,
168         customView: View?,
169     ): View? {
170         return if (smartspaceDataPlugin != null) {
171             val view =
172                 smartspaceViewComponentFactory
173                     .create(parent, smartspaceDataPlugin, stateChangeListener, customView)
174                     .getView()
175             if (view !is View) {
176                 return null
177             }
178             return view
179         } else {
180             null
181         }
182     }
183 
184     private fun hasActiveSessionListeners(): Boolean {
185         return smartspaceViews.isNotEmpty() ||
186             listeners.isNotEmpty() ||
187             unfilteredListeners.isNotEmpty()
188     }
189 
190     private fun connectSession() {
191         if (userSmartspaceManager == null) {
192             userSmartspaceManager =
193                 userTracker.userContext.getSystemService(SmartspaceManager::class.java)
194         }
195         if (userSmartspaceManager == null) {
196             return
197         }
198         if (plugin == null && weatherPlugin == null) {
199             return
200         }
201         if (session != null || !hasActiveSessionListeners()) {
202             return
203         }
204 
205         if (!precondition.conditionsMet()) {
206             return
207         }
208 
209         val newSession =
210             userSmartspaceManager?.createSmartspaceSession(
211                 SmartspaceConfig.Builder(userTracker.userContext, UI_SURFACE_DREAM).build()
212             )
213         Log.d(TAG, "Starting smartspace session for dream")
214         newSession?.addOnTargetsAvailableListener(uiExecutor, sessionListener)
215         this.session = newSession
216 
217         weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
218         plugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
219 
220         reloadSmartspace()
221     }
222 
223     /** Disconnects the smartspace view from the smartspace service and cleans up any resources. */
224     private fun disconnect() {
225         if (hasActiveSessionListeners()) return
226 
227         execution.assertIsMainThread()
228 
229         if (session == null) {
230             return
231         }
232 
233         session?.let {
234             it.removeOnTargetsAvailableListener(sessionListener)
235             it.close()
236         }
237 
238         session = null
239 
240         weatherPlugin?.registerSmartspaceEventNotifier(null)
241         weatherPlugin?.onTargetsAvailable(emptyList())
242 
243         plugin?.registerSmartspaceEventNotifier(null)
244         plugin?.onTargetsAvailable(emptyList())
245         Log.d(TAG, "Ending smartspace session for dream")
246     }
247 
248     fun addListener(listener: SmartspaceTargetListener) {
249         addAndRegisterListener(listener, plugin)
250     }
251 
252     fun removeListener(listener: SmartspaceTargetListener) {
253         removeAndUnregisterListener(listener, plugin)
254     }
255 
256     fun addListenerForWeatherPlugin(listener: SmartspaceTargetListener) {
257         addAndRegisterListener(listener, weatherPlugin)
258     }
259 
260     fun removeListenerForWeatherPlugin(listener: SmartspaceTargetListener) {
261         removeAndUnregisterListener(listener, weatherPlugin)
262     }
263 
264     private fun addAndRegisterListener(
265         listener: SmartspaceTargetListener,
266         smartspaceDataPlugin: BcSmartspaceDataPlugin?,
267     ) {
268         execution.assertIsMainThread()
269         smartspaceDataPlugin?.registerListener(listener)
270         listeners.add(listener)
271 
272         connectSession()
273     }
274 
275     private fun removeAndUnregisterListener(
276         listener: SmartspaceTargetListener,
277         smartspaceDataPlugin: BcSmartspaceDataPlugin?,
278     ) {
279         execution.assertIsMainThread()
280         smartspaceDataPlugin?.unregisterListener(listener)
281         listeners.remove(listener)
282         disconnect()
283     }
284 
285     private fun reloadSmartspace() {
286         session?.requestSmartspaceUpdate()
287     }
288 
289     private fun onTargetsAvailableUnfiltered(targets: List<SmartspaceTarget>) {
290         unfilteredListeners.forEach { it.onSmartspaceTargetsUpdated(targets) }
291     }
292 
293     /**
294      * Adds a listener for the raw, unfiltered list of smartspace targets. This should be used
295      * carefully, as it doesn't filter out targets which the user may not want shown.
296      */
297     fun addUnfilteredListener(listener: SmartspaceTargetListener) {
298         unfilteredListeners.add(listener)
299         connectSession()
300     }
301 
302     fun removeUnfilteredListener(listener: SmartspaceTargetListener) {
303         unfilteredListeners.remove(listener)
304         disconnect()
305     }
306 }
307