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