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