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