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