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