• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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