1 /* <lambda>null2 * Copyright (C) 2021 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.statusbar.lockscreen 18 19 import android.app.PendingIntent 20 import android.app.WallpaperManager 21 import android.app.smartspace.SmartspaceConfig 22 import android.app.smartspace.SmartspaceManager 23 import android.app.smartspace.SmartspaceSession 24 import android.app.smartspace.SmartspaceTarget 25 import android.content.ContentResolver 26 import android.content.Context 27 import android.content.Intent 28 import android.database.ContentObserver 29 import android.net.Uri 30 import android.os.Handler 31 import android.os.UserHandle 32 import android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS 33 import android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS 34 import android.provider.Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED 35 import android.util.Log 36 import android.view.ContextThemeWrapper 37 import android.view.View 38 import android.view.ViewGroup 39 import com.android.keyguard.KeyguardUpdateMonitor 40 import com.android.settingslib.Utils 41 import com.android.systemui.Dumpable 42 import com.android.systemui.R 43 import com.android.systemui.dagger.SysUISingleton 44 import com.android.systemui.dagger.qualifiers.Background 45 import com.android.systemui.dagger.qualifiers.Main 46 import com.android.systemui.dump.DumpManager 47 import com.android.systemui.flags.FeatureFlags 48 import com.android.systemui.flags.Flags 49 import com.android.systemui.plugins.ActivityStarter 50 import com.android.systemui.plugins.BcSmartspaceConfigPlugin 51 import com.android.systemui.plugins.BcSmartspaceDataPlugin 52 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener 53 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView 54 import com.android.systemui.plugins.FalsingManager 55 import com.android.systemui.plugins.WeatherData 56 import com.android.systemui.plugins.statusbar.StatusBarStateController 57 import com.android.systemui.settings.UserTracker 58 import com.android.systemui.shared.regionsampling.RegionSampler 59 import com.android.systemui.shared.regionsampling.UpdateColorCallback 60 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DATE_SMARTSPACE_DATA_PLUGIN 61 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.WEATHER_SMARTSPACE_DATA_PLUGIN 62 import com.android.systemui.statusbar.phone.KeyguardBypassController 63 import com.android.systemui.statusbar.policy.ConfigurationController 64 import com.android.systemui.statusbar.policy.DeviceProvisionedController 65 import com.android.systemui.util.concurrency.Execution 66 import com.android.systemui.util.settings.SecureSettings 67 import com.android.systemui.util.time.SystemClock 68 import java.io.PrintWriter 69 import java.time.Instant 70 import java.util.Optional 71 import java.util.concurrent.Executor 72 import javax.inject.Inject 73 import javax.inject.Named 74 75 /** Controller for managing the smartspace view on the lockscreen */ 76 @SysUISingleton 77 class LockscreenSmartspaceController 78 @Inject 79 constructor( 80 private val context: Context, 81 private val featureFlags: FeatureFlags, 82 private val smartspaceManager: SmartspaceManager, 83 private val activityStarter: ActivityStarter, 84 private val falsingManager: FalsingManager, 85 private val systemClock: SystemClock, 86 private val secureSettings: SecureSettings, 87 private val userTracker: UserTracker, 88 private val contentResolver: ContentResolver, 89 private val configurationController: ConfigurationController, 90 private val statusBarStateController: StatusBarStateController, 91 private val deviceProvisionedController: DeviceProvisionedController, 92 private val bypassController: KeyguardBypassController, 93 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 94 private val dumpManager: DumpManager, 95 private val execution: Execution, 96 @Main private val uiExecutor: Executor, 97 @Background private val bgExecutor: Executor, 98 @Main private val handler: Handler, 99 @Named(DATE_SMARTSPACE_DATA_PLUGIN) 100 optionalDatePlugin: Optional<BcSmartspaceDataPlugin>, 101 @Named(WEATHER_SMARTSPACE_DATA_PLUGIN) 102 optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>, 103 optionalPlugin: Optional<BcSmartspaceDataPlugin>, 104 optionalConfigPlugin: Optional<BcSmartspaceConfigPlugin>, 105 ) : Dumpable { 106 companion object { 107 private const val TAG = "LockscreenSmartspaceController" 108 } 109 110 private var session: SmartspaceSession? = null 111 private val datePlugin: BcSmartspaceDataPlugin? = optionalDatePlugin.orElse(null) 112 private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null) 113 private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null) 114 private val configPlugin: BcSmartspaceConfigPlugin? = optionalConfigPlugin.orElse(null) 115 116 // Smartspace can be used on multiple displays, such as when the user casts their screen 117 private var smartspaceViews = mutableSetOf<SmartspaceView>() 118 private var regionSamplers = 119 mutableMapOf<SmartspaceView, RegionSampler>() 120 121 private val regionSamplingEnabled = 122 featureFlags.isEnabled(Flags.REGION_SAMPLING) 123 private var isContentUpdatedOnce = false 124 private var showNotifications = false 125 private var showSensitiveContentForCurrentUser = false 126 private var showSensitiveContentForManagedUser = false 127 private var managedUserHandle: UserHandle? = null 128 129 // TODO(b/202758428): refactor so that we can test color updates via region samping, similar to 130 // how we test color updates when theme changes (See testThemeChangeUpdatesTextColor). 131 private val updateFun: UpdateColorCallback = { updateTextColorFromRegionSampler() } 132 133 var stateChangeListener = object : View.OnAttachStateChangeListener { 134 override fun onViewAttachedToWindow(v: View) { 135 smartspaceViews.add(v as SmartspaceView) 136 137 connectSession() 138 139 updateTextColorFromWallpaper() 140 statusBarStateListener.onDozeAmountChanged(0f, statusBarStateController.dozeAmount) 141 } 142 143 override fun onViewDetachedFromWindow(v: View) { 144 smartspaceViews.remove(v as SmartspaceView) 145 146 if (smartspaceViews.isEmpty()) { 147 disconnect() 148 } 149 } 150 } 151 152 private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets -> 153 execution.assertIsMainThread() 154 155 // The weather data plugin takes unfiltered targets and performs the filtering internally. 156 weatherPlugin?.onTargetsAvailable(targets) 157 val now = Instant.ofEpochMilli(systemClock.currentTimeMillis()) 158 val weatherTarget = targets.find { t -> 159 t.featureType == SmartspaceTarget.FEATURE_WEATHER && 160 now.isAfter(Instant.ofEpochMilli(t.creationTimeMillis)) && 161 now.isBefore(Instant.ofEpochMilli(t.expiryTimeMillis)) 162 } 163 if (weatherTarget != null) { 164 val weatherData = WeatherData.fromBundle(weatherTarget.baseAction.extras) 165 if (weatherData != null) { 166 keyguardUpdateMonitor.sendWeatherData(weatherData) 167 } 168 } 169 170 val filteredTargets = targets.filter(::filterSmartspaceTarget) 171 plugin?.onTargetsAvailable(filteredTargets) 172 if (!isContentUpdatedOnce) { 173 for (v in smartspaceViews) { 174 if (regionSamplingEnabled) { 175 var regionSampler = RegionSampler( 176 v as View, 177 uiExecutor, 178 bgExecutor, 179 regionSamplingEnabled, 180 updateFun 181 ) 182 initializeTextColors(regionSampler) 183 regionSamplers[v] = regionSampler 184 regionSampler.startRegionSampler() 185 } 186 updateTextColorFromWallpaper() 187 } 188 isContentUpdatedOnce = true 189 } 190 } 191 192 private val userTrackerCallback = object : UserTracker.Callback { 193 override fun onUserChanged(newUser: Int, userContext: Context) { 194 execution.assertIsMainThread() 195 reloadSmartspace() 196 } 197 } 198 199 private val settingsObserver = object : ContentObserver(handler) { 200 override fun onChange(selfChange: Boolean, uri: Uri?) { 201 execution.assertIsMainThread() 202 reloadSmartspace() 203 } 204 } 205 206 private val configChangeListener = object : ConfigurationController.ConfigurationListener { 207 override fun onThemeChanged() { 208 execution.assertIsMainThread() 209 updateTextColorFromWallpaper() 210 } 211 } 212 213 private val statusBarStateListener = object : StatusBarStateController.StateListener { 214 override fun onDozeAmountChanged(linear: Float, eased: Float) { 215 execution.assertIsMainThread() 216 smartspaceViews.forEach { it.setDozeAmount(eased) } 217 } 218 } 219 220 private val deviceProvisionedListener = 221 object : DeviceProvisionedController.DeviceProvisionedListener { 222 override fun onDeviceProvisionedChanged() { 223 connectSession() 224 } 225 226 override fun onUserSetupChanged() { 227 connectSession() 228 } 229 } 230 231 private val bypassStateChangedListener = 232 object : KeyguardBypassController.OnBypassStateChangedListener { 233 override fun onBypassStateChanged(isEnabled: Boolean) { 234 updateBypassEnabled() 235 } 236 } 237 238 init { 239 deviceProvisionedController.addCallback(deviceProvisionedListener) 240 dumpManager.registerDumpable(this) 241 } 242 243 fun isEnabled(): Boolean { 244 execution.assertIsMainThread() 245 246 return plugin != null 247 } 248 249 fun isDateWeatherDecoupled(): Boolean { 250 execution.assertIsMainThread() 251 252 return featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED) && 253 datePlugin != null && weatherPlugin != null 254 } 255 256 fun isWeatherEnabled(): Boolean { 257 execution.assertIsMainThread() 258 val defaultValue = context.getResources().getBoolean( 259 com.android.internal.R.bool.config_lockscreenWeatherEnabledByDefault) 260 val showWeather = secureSettings.getIntForUser( 261 LOCK_SCREEN_WEATHER_ENABLED, 262 if (defaultValue) 1 else 0, 263 userTracker.userId) == 1 264 return showWeather 265 } 266 267 private fun updateBypassEnabled() { 268 val bypassEnabled = bypassController.bypassEnabled 269 smartspaceViews.forEach { it.setKeyguardBypassEnabled(bypassEnabled) } 270 } 271 272 /** 273 * Constructs the date view and connects it to the smartspace service. 274 */ 275 fun buildAndConnectDateView(parent: ViewGroup): View? { 276 execution.assertIsMainThread() 277 278 if (!isEnabled()) { 279 throw RuntimeException("Cannot build view when not enabled") 280 } 281 if (!isDateWeatherDecoupled()) { 282 throw RuntimeException("Cannot build date view when not decoupled") 283 } 284 285 val view = buildView(parent, datePlugin) 286 connectSession() 287 288 return view 289 } 290 291 /** 292 * Constructs the weather view and connects it to the smartspace service. 293 */ 294 fun buildAndConnectWeatherView(parent: ViewGroup): View? { 295 execution.assertIsMainThread() 296 297 if (!isEnabled()) { 298 throw RuntimeException("Cannot build view when not enabled") 299 } 300 if (!isDateWeatherDecoupled()) { 301 throw RuntimeException("Cannot build weather view when not decoupled") 302 } 303 304 val view = buildView(parent, weatherPlugin) 305 connectSession() 306 307 return view 308 } 309 310 /** 311 * Constructs the smartspace view and connects it to the smartspace service. 312 */ 313 fun buildAndConnectView(parent: ViewGroup): View? { 314 execution.assertIsMainThread() 315 316 if (!isEnabled()) { 317 throw RuntimeException("Cannot build view when not enabled") 318 } 319 320 val view = buildView(parent, plugin, configPlugin) 321 connectSession() 322 323 return view 324 } 325 326 private fun buildView( 327 parent: ViewGroup, 328 plugin: BcSmartspaceDataPlugin?, 329 configPlugin: BcSmartspaceConfigPlugin? = null 330 ): View? { 331 if (plugin == null) { 332 return null 333 } 334 335 val ssView = plugin.getView(parent) 336 configPlugin?.let { ssView.registerConfigProvider(it) } 337 ssView.setUiSurface(BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD) 338 ssView.registerDataProvider(plugin) 339 340 ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter { 341 override fun startIntent(view: View, intent: Intent, showOnLockscreen: Boolean) { 342 if (showOnLockscreen) { 343 activityStarter.startActivity( 344 intent, 345 true, /* dismissShade */ 346 // launch animator - looks bad with the transparent smartspace bg 347 null, 348 true 349 ) 350 } else { 351 activityStarter.postStartActivityDismissingKeyguard(intent, 0) 352 } 353 } 354 355 override fun startPendingIntent(pi: PendingIntent, showOnLockscreen: Boolean) { 356 if (showOnLockscreen) { 357 pi.send() 358 } else { 359 activityStarter.postStartActivityDismissingKeyguard(pi) 360 } 361 } 362 }) 363 ssView.setFalsingManager(falsingManager) 364 ssView.setKeyguardBypassEnabled(bypassController.bypassEnabled) 365 return (ssView as View).apply { addOnAttachStateChangeListener(stateChangeListener) } 366 } 367 368 private fun connectSession() { 369 if (datePlugin == null && weatherPlugin == null && plugin == null) return 370 if (session != null || smartspaceViews.isEmpty()) { 371 return 372 } 373 374 // Only connect after the device is fully provisioned to avoid connection caching 375 // issues 376 if (!deviceProvisionedController.isDeviceProvisioned() || 377 !deviceProvisionedController.isCurrentUserSetup()) { 378 return 379 } 380 381 val newSession = smartspaceManager.createSmartspaceSession( 382 SmartspaceConfig.Builder( 383 context, BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD).build()) 384 Log.d(TAG, "Starting smartspace session for " + 385 BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD) 386 newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener) 387 this.session = newSession 388 389 deviceProvisionedController.removeCallback(deviceProvisionedListener) 390 userTracker.addCallback(userTrackerCallback, uiExecutor) 391 contentResolver.registerContentObserver( 392 secureSettings.getUriFor(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS), 393 true, 394 settingsObserver, 395 UserHandle.USER_ALL 396 ) 397 contentResolver.registerContentObserver( 398 secureSettings.getUriFor(LOCK_SCREEN_SHOW_NOTIFICATIONS), 399 true, 400 settingsObserver, 401 UserHandle.USER_ALL 402 ) 403 configurationController.addCallback(configChangeListener) 404 statusBarStateController.addCallback(statusBarStateListener) 405 bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener) 406 407 datePlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) } 408 weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) } 409 plugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) } 410 411 updateBypassEnabled() 412 reloadSmartspace() 413 } 414 415 /** 416 * Requests the smartspace session for an update. 417 */ 418 fun requestSmartspaceUpdate() { 419 session?.requestSmartspaceUpdate() 420 } 421 422 /** 423 * Disconnects the smartspace view from the smartspace service and cleans up any resources. 424 */ 425 fun disconnect() { 426 if (!smartspaceViews.isEmpty()) return 427 428 execution.assertIsMainThread() 429 430 if (session == null) { 431 return 432 } 433 434 session?.let { 435 it.removeOnTargetsAvailableListener(sessionListener) 436 it.close() 437 } 438 userTracker.removeCallback(userTrackerCallback) 439 contentResolver.unregisterContentObserver(settingsObserver) 440 configurationController.removeCallback(configChangeListener) 441 statusBarStateController.removeCallback(statusBarStateListener) 442 bypassController.unregisterOnBypassStateChangedListener(bypassStateChangedListener) 443 session = null 444 445 datePlugin?.registerSmartspaceEventNotifier(null) 446 447 weatherPlugin?.registerSmartspaceEventNotifier(null) 448 weatherPlugin?.onTargetsAvailable(emptyList()) 449 450 plugin?.registerSmartspaceEventNotifier(null) 451 plugin?.onTargetsAvailable(emptyList()) 452 453 Log.d(TAG, "Ended smartspace session for lockscreen") 454 } 455 456 fun addListener(listener: SmartspaceTargetListener) { 457 execution.assertIsMainThread() 458 plugin?.registerListener(listener) 459 } 460 461 fun removeListener(listener: SmartspaceTargetListener) { 462 execution.assertIsMainThread() 463 plugin?.unregisterListener(listener) 464 } 465 466 private fun filterSmartspaceTarget(t: SmartspaceTarget): Boolean { 467 if (isDateWeatherDecoupled()) { 468 return t.featureType != SmartspaceTarget.FEATURE_WEATHER 469 } 470 if (!showNotifications) { 471 return t.featureType == SmartspaceTarget.FEATURE_WEATHER 472 } 473 return when (t.userHandle) { 474 userTracker.userHandle -> { 475 !t.isSensitive || showSensitiveContentForCurrentUser 476 } 477 managedUserHandle -> { 478 // Really, this should be "if this managed profile is associated with the current 479 // active user", but we don't have a good way to check that, so instead we cheat: 480 // Only the primary user can have an associated managed profile, so only show 481 // content for the managed profile if the primary user is active 482 userTracker.userHandle.identifier == UserHandle.USER_SYSTEM && 483 (!t.isSensitive || showSensitiveContentForManagedUser) 484 } 485 else -> { 486 false 487 } 488 } 489 } 490 491 private fun initializeTextColors(regionSampler: RegionSampler) { 492 val lightThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_LightWallpaper) 493 val darkColor = Utils.getColorAttrDefaultColor(lightThemeContext, R.attr.wallpaperTextColor) 494 495 val darkThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI) 496 val lightColor = Utils.getColorAttrDefaultColor(darkThemeContext, R.attr.wallpaperTextColor) 497 498 regionSampler.setForegroundColors(lightColor, darkColor) 499 } 500 501 private fun updateTextColorFromRegionSampler() { 502 smartspaceViews.forEach { 503 val textColor = regionSamplers.get(it)?.currentForegroundColor() 504 if (textColor != null) { 505 it.setPrimaryTextColor(textColor) 506 } 507 } 508 } 509 510 private fun updateTextColorFromWallpaper() { 511 val wallpaperManager = WallpaperManager.getInstance(context) 512 if (!regionSamplingEnabled || wallpaperManager.lockScreenWallpaperExists() || 513 regionSamplers.isEmpty()) { 514 val wallpaperTextColor = 515 Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor) 516 smartspaceViews.forEach { it.setPrimaryTextColor(wallpaperTextColor) } 517 } else { 518 updateTextColorFromRegionSampler() 519 } 520 } 521 522 private fun reloadSmartspace() { 523 showNotifications = secureSettings.getIntForUser( 524 LOCK_SCREEN_SHOW_NOTIFICATIONS, 525 0, 526 userTracker.userId 527 ) == 1 528 529 showSensitiveContentForCurrentUser = secureSettings.getIntForUser( 530 LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 531 0, 532 userTracker.userId 533 ) == 1 534 535 managedUserHandle = getWorkProfileUser() 536 val managedId = managedUserHandle?.identifier 537 if (managedId != null) { 538 showSensitiveContentForManagedUser = secureSettings.getIntForUser( 539 LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 540 0, 541 managedId 542 ) == 1 543 } 544 545 session?.requestSmartspaceUpdate() 546 } 547 548 private fun getWorkProfileUser(): UserHandle? { 549 for (userInfo in userTracker.userProfiles) { 550 if (userInfo.isManagedProfile) { 551 return userInfo.userHandle 552 } 553 } 554 return null 555 } 556 557 override fun dump(pw: PrintWriter, args: Array<out String>) { 558 pw.println("Region Samplers: ${regionSamplers.size}") 559 regionSamplers.map { (_, sampler) -> 560 sampler.dump(pw) 561 } 562 } 563 } 564