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