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.PendingIntent
20 import android.app.smartspace.SmartspaceConfig
21 import android.app.smartspace.SmartspaceManager
22 import android.app.smartspace.SmartspaceSession
23 import android.app.smartspace.SmartspaceTarget
24 import android.content.ContentResolver
25 import android.content.Context
26 import android.content.Intent
27 import android.database.ContentObserver
28 import android.net.Uri
29 import android.os.Handler
30 import android.os.UserHandle
31 import android.provider.Settings
32 import android.util.Log
33 import android.view.View
34 import android.view.ViewGroup
35 import com.android.settingslib.Utils
36 import com.android.systemui.R
37 import com.android.systemui.dagger.SysUISingleton
38 import com.android.systemui.dagger.qualifiers.Main
39 import com.android.systemui.plugins.ActivityStarter
40 import com.android.systemui.plugins.BcSmartspaceDataPlugin
41 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
42 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
43 import com.android.systemui.plugins.FalsingManager
44 import com.android.systemui.plugins.statusbar.StatusBarStateController
45 import com.android.systemui.settings.UserTracker
46 import com.android.systemui.flags.FeatureFlags
47 import com.android.systemui.statusbar.policy.ConfigurationController
48 import com.android.systemui.statusbar.policy.DeviceProvisionedController
49 import com.android.systemui.util.concurrency.Execution
50 import com.android.systemui.util.settings.SecureSettings
51 import java.lang.RuntimeException
52 import java.util.Optional
53 import java.util.concurrent.Executor
54 import javax.inject.Inject
55 
56 /**
57  * Controller for managing the smartspace view on the lockscreen
58  */
59 @SysUISingleton
60 class LockscreenSmartspaceController @Inject constructor(
61     private val context: Context,
62     private val featureFlags: FeatureFlags,
63     private val smartspaceManager: SmartspaceManager,
64     private val activityStarter: ActivityStarter,
65     private val falsingManager: FalsingManager,
66     private val secureSettings: SecureSettings,
67     private val userTracker: UserTracker,
68     private val contentResolver: ContentResolver,
69     private val configurationController: ConfigurationController,
70     private val statusBarStateController: StatusBarStateController,
71     private val deviceProvisionedController: DeviceProvisionedController,
72     private val execution: Execution,
73     @Main private val uiExecutor: Executor,
74     @Main private val handler: Handler,
75     optionalPlugin: Optional<BcSmartspaceDataPlugin>
76 ) {
77     companion object {
78         private const val TAG = "LockscreenSmartspaceController"
79     }
80 
81     private var session: SmartspaceSession? = null
82     private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
83 
84     // Smartspace can be used on multiple displays, such as when the user casts their screen
85     private var smartspaceViews = mutableSetOf<SmartspaceView>()
86 
87     private var showSensitiveContentForCurrentUser = false
88     private var showSensitiveContentForManagedUser = false
89     private var managedUserHandle: UserHandle? = null
90 
91     var stateChangeListener = object : View.OnAttachStateChangeListener {
92         override fun onViewAttachedToWindow(v: View) {
93             smartspaceViews.add(v as SmartspaceView)
94             connectSession()
95 
96             updateTextColorFromWallpaper()
97             statusBarStateListener.onDozeAmountChanged(0f, statusBarStateController.dozeAmount)
98         }
99 
100         override fun onViewDetachedFromWindow(v: View) {
101             smartspaceViews.remove(v as SmartspaceView)
102 
103             if (smartspaceViews.isEmpty()) {
104                 disconnect()
105             }
106         }
107     }
108 
109     private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets ->
110         execution.assertIsMainThread()
111         val filteredTargets = targets.filter(::filterSmartspaceTarget)
112         plugin?.onTargetsAvailable(filteredTargets)
113     }
114 
115     private val userTrackerCallback = object : UserTracker.Callback {
116         override fun onUserChanged(newUser: Int, userContext: Context) {
117             execution.assertIsMainThread()
118             reloadSmartspace()
119         }
120     }
121 
122     private val settingsObserver = object : ContentObserver(handler) {
123         override fun onChange(selfChange: Boolean, uri: Uri?) {
124             execution.assertIsMainThread()
125             reloadSmartspace()
126         }
127     }
128 
129     private val configChangeListener = object : ConfigurationController.ConfigurationListener {
130         override fun onThemeChanged() {
131             execution.assertIsMainThread()
132             updateTextColorFromWallpaper()
133         }
134     }
135 
136     private val statusBarStateListener = object : StatusBarStateController.StateListener {
137         override fun onDozeAmountChanged(linear: Float, eased: Float) {
138             execution.assertIsMainThread()
139             smartspaceViews.forEach { it.setDozeAmount(eased) }
140         }
141     }
142 
143     private val deviceProvisionedListener =
144         object : DeviceProvisionedController.DeviceProvisionedListener {
145             override fun onDeviceProvisionedChanged() {
146                 connectSession()
147             }
148 
149             override fun onUserSetupChanged() {
150                 connectSession()
151             }
152         }
153 
154     init {
155         deviceProvisionedController.addCallback(deviceProvisionedListener)
156     }
157 
158     fun isEnabled(): Boolean {
159         execution.assertIsMainThread()
160 
161         return featureFlags.isSmartspaceEnabled && plugin != null
162     }
163 
164     /**
165      * Constructs the smartspace view and connects it to the smartspace service.
166      */
167     fun buildAndConnectView(parent: ViewGroup): View? {
168         execution.assertIsMainThread()
169 
170         if (!isEnabled()) {
171             throw RuntimeException("Cannot build view when not enabled")
172         }
173 
174         val view = buildView(parent)
175         connectSession()
176 
177         return view
178     }
179 
180     fun requestSmartspaceUpdate() {
181         session?.requestSmartspaceUpdate()
182     }
183 
184     private fun buildView(parent: ViewGroup): View? {
185         if (plugin == null) {
186             return null
187         }
188 
189         val ssView = plugin.getView(parent)
190         ssView.registerDataProvider(plugin)
191 
192         ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter {
193             override fun startIntent(view: View, intent: Intent, showOnLockscreen: Boolean) {
194                 activityStarter.startActivity(
195                     intent,
196                     true, /* dismissShade */
197                     null, /* launch animator - looks bad with the transparent smartspace bg */
198                     showOnLockscreen
199                 )
200             }
201 
202             override fun startPendingIntent(pi: PendingIntent, showOnLockscreen: Boolean) {
203                 if (showOnLockscreen) {
204                     pi.send()
205                 } else {
206                     activityStarter.startPendingIntentDismissingKeyguard(pi)
207                 }
208             }
209         })
210         ssView.setFalsingManager(falsingManager)
211         return (ssView as View).apply { addOnAttachStateChangeListener(stateChangeListener) }
212     }
213 
214     private fun connectSession() {
215         if (plugin == null || session != null || smartspaceViews.isEmpty()) {
216             return
217         }
218 
219         // Only connect after the device is fully provisioned to avoid connection caching
220         // issues
221         if (!deviceProvisionedController.isDeviceProvisioned() ||
222                 !deviceProvisionedController.isCurrentUserSetup()) {
223             return
224         }
225 
226         val newSession = smartspaceManager.createSmartspaceSession(
227                 SmartspaceConfig.Builder(context, "lockscreen").build())
228         Log.d(TAG, "Starting smartspace session for lockscreen")
229         newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener)
230         this.session = newSession
231 
232         deviceProvisionedController.removeCallback(deviceProvisionedListener)
233         userTracker.addCallback(userTrackerCallback, uiExecutor)
234         contentResolver.registerContentObserver(
235                 secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS),
236                 true,
237                 settingsObserver,
238                 UserHandle.USER_ALL
239         )
240         configurationController.addCallback(configChangeListener)
241         statusBarStateController.addCallback(statusBarStateListener)
242 
243         plugin.registerSmartspaceEventNotifier {
244                 e -> session?.notifySmartspaceEvent(e)
245         }
246 
247         reloadSmartspace()
248     }
249 
250     /**
251      * Disconnects the smartspace view from the smartspace service and cleans up any resources.
252      */
253     fun disconnect() {
254         if (!smartspaceViews.isEmpty()) return
255 
256         execution.assertIsMainThread()
257 
258         if (session == null) {
259             return
260         }
261 
262         session?.let {
263             it.removeOnTargetsAvailableListener(sessionListener)
264             it.close()
265         }
266         userTracker.removeCallback(userTrackerCallback)
267         contentResolver.unregisterContentObserver(settingsObserver)
268         configurationController.removeCallback(configChangeListener)
269         statusBarStateController.removeCallback(statusBarStateListener)
270         session = null
271 
272         plugin?.registerSmartspaceEventNotifier(null)
273         plugin?.onTargetsAvailable(emptyList())
274         Log.d(TAG, "Ending smartspace session for lockscreen")
275     }
276 
277     fun addListener(listener: SmartspaceTargetListener) {
278         execution.assertIsMainThread()
279         plugin?.registerListener(listener)
280     }
281 
282     fun removeListener(listener: SmartspaceTargetListener) {
283         execution.assertIsMainThread()
284         plugin?.unregisterListener(listener)
285     }
286 
287     private fun filterSmartspaceTarget(t: SmartspaceTarget): Boolean {
288         return when (t.userHandle) {
289             userTracker.userHandle -> {
290                 !t.isSensitive || showSensitiveContentForCurrentUser
291             }
292             managedUserHandle -> {
293                 // Really, this should be "if this managed profile is associated with the current
294                 // active user", but we don't have a good way to check that, so instead we cheat:
295                 // Only the primary user can have an associated managed profile, so only show
296                 // content for the managed profile if the primary user is active
297                 userTracker.userHandle.identifier == UserHandle.USER_SYSTEM &&
298                         (!t.isSensitive || showSensitiveContentForManagedUser)
299             }
300             else -> {
301                 false
302             }
303         }
304     }
305 
306     private fun updateTextColorFromWallpaper() {
307         val wallpaperTextColor = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor)
308         smartspaceViews.forEach { it.setPrimaryTextColor(wallpaperTextColor) }
309     }
310 
311     private fun reloadSmartspace() {
312         val setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
313 
314         showSensitiveContentForCurrentUser =
315                 secureSettings.getIntForUser(setting, 0, userTracker.userId) == 1
316 
317         managedUserHandle = getWorkProfileUser()
318         val managedId = managedUserHandle?.identifier
319         if (managedId != null) {
320             showSensitiveContentForManagedUser =
321                     secureSettings.getIntForUser(setting, 0, managedId) == 1
322         }
323 
324         session?.requestSmartspaceUpdate()
325     }
326 
327     private fun getWorkProfileUser(): UserHandle? {
328         for (userInfo in userTracker.userProfiles) {
329             if (userInfo.isManagedProfile) {
330                 return userInfo.userHandle
331             }
332         }
333         return null
334     }
335 }
336