1 /* 2 * Copyright (C) 2020 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.controls.controller 18 19 import android.annotation.WorkerThread 20 import android.content.ComponentName 21 import android.content.Context 22 import android.content.Intent 23 import android.content.ServiceConnection 24 import android.os.Binder 25 import android.os.Bundle 26 import android.os.IBinder 27 import android.os.UserHandle 28 import android.service.controls.ControlsProviderService 29 import android.service.controls.ControlsProviderService.CALLBACK_BUNDLE 30 import android.service.controls.ControlsProviderService.CALLBACK_TOKEN 31 import android.service.controls.IControlsActionCallback 32 import android.service.controls.IControlsProvider 33 import android.service.controls.IControlsSubscriber 34 import android.service.controls.IControlsSubscription 35 import android.service.controls.actions.ControlAction 36 import android.util.ArraySet 37 import android.util.Log 38 import com.android.internal.annotations.GuardedBy 39 import com.android.systemui.util.concurrency.DelayableExecutor 40 import java.util.concurrent.TimeUnit 41 import java.util.concurrent.atomic.AtomicBoolean 42 43 /** 44 * Manager for the lifecycle of the connection to a given [ControlsProviderService]. 45 * 46 * This class handles binding and unbinding and requests to the service. The class will queue 47 * requests until the service is connected and dispatch them then. 48 * 49 * If the provider app is updated, and we are currently bound to it, it will try to rebind after 50 * update is completed. 51 * 52 * @property context A SystemUI context for binding to the services 53 * @property executor A delayable executor for posting timeouts 54 * @property actionCallbackService a callback interface to hand the remote service for sending 55 * action responses 56 * @property subscriberService an "subscriber" interface for requesting and accepting updates for 57 * controls from the service. 58 * @property user the user for whose this service should be bound. 59 * @property componentName the name of the component for the service. 60 */ 61 class ControlsProviderLifecycleManager( 62 private val context: Context, 63 private val executor: DelayableExecutor, 64 private val actionCallbackService: IControlsActionCallback.Stub, 65 val user: UserHandle, 66 val componentName: ComponentName, 67 packageUpdateMonitorFactory: PackageUpdateMonitor.Factory, 68 ) { 69 70 val token: IBinder = Binder() 71 private var requiresBound = false 72 @GuardedBy("queuedServiceMethods") 73 private val queuedServiceMethods: MutableSet<ServiceMethod> = ArraySet() 74 private var wrapper: ServiceWrapper? = null 75 private val TAG = javaClass.simpleName 76 private var onLoadCanceller: Runnable? = null 77 78 private var lastForPanel = false 79 80 companion object { 81 private const val LOAD_TIMEOUT_SECONDS = 20L // seconds 82 private const val DEBUG = true 83 private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or 84 Context.BIND_NOT_PERCEPTIBLE 85 // Use BIND_NOT_PERCEPTIBLE so it will be at lower priority from SystemUI. 86 // However, don't use WAIVE_PRIORITY, as by itself, it will kill the app 87 // once the Task is finished in the device controls panel. 88 private val BIND_FLAGS_PANEL = Context.BIND_AUTO_CREATE or Context.BIND_NOT_PERCEPTIBLE 89 } 90 <lambda>null91 private val intent = Intent(ControlsProviderService.SERVICE_CONTROLS).apply { 92 component = componentName 93 putExtra(CALLBACK_BUNDLE, Bundle().apply { 94 putBinder(CALLBACK_TOKEN, token) 95 }) 96 } 97 98 private val packageUpdateMonitor = packageUpdateMonitorFactory.create( 99 user, 100 componentName.packageName, <lambda>null101 ) { 102 if (requiresBound) { 103 // Let's unbind just in case. onBindingDied should have been called and unbound before. 104 executor.execute { 105 unbindAndCleanup("package updated") 106 bindService(true, lastForPanel) 107 } 108 } 109 } 110 bindServicenull111 private fun bindService(bind: Boolean, forPanel: Boolean = false) { 112 executor.execute { 113 bindServiceBackground(bind, forPanel) 114 } 115 } 116 117 private val serviceConnection = object : ServiceConnection { 118 119 val connected = AtomicBoolean(false) 120 onServiceConnectednull121 override fun onServiceConnected(name: ComponentName, service: IBinder) { 122 if (DEBUG) Log.d(TAG, "onServiceConnected $name") 123 wrapper = ServiceWrapper(IControlsProvider.Stub.asInterface(service)) 124 packageUpdateMonitor.startMonitoring() 125 handlePendingServiceMethods() 126 } 127 onServiceDisconnectednull128 override fun onServiceDisconnected(name: ComponentName?) { 129 if (DEBUG) Log.d(TAG, "onServiceDisconnected $name") 130 wrapper = null 131 // No need to call unbind. We may get a new `onServiceConnected` 132 } 133 onNullBindingnull134 override fun onNullBinding(name: ComponentName?) { 135 if (DEBUG) Log.d(TAG, "onNullBinding $name") 136 wrapper = null 137 executor.execute { 138 unbindAndCleanup("null binding") 139 } 140 } 141 onBindingDiednull142 override fun onBindingDied(name: ComponentName?) { 143 super.onBindingDied(name) 144 if (DEBUG) Log.d(TAG, "onBindingDied $name") 145 executor.execute { 146 unbindAndCleanup("binder died") 147 } 148 } 149 } 150 handlePendingServiceMethodsnull151 private fun handlePendingServiceMethods() { 152 val queue = synchronized(queuedServiceMethods) { 153 ArraySet(queuedServiceMethods).also { 154 queuedServiceMethods.clear() 155 } 156 } 157 queue.forEach { 158 it.run() 159 } 160 } 161 162 @WorkerThread bindServiceBackgroundnull163 private fun bindServiceBackground(bind: Boolean, forPanel: Boolean = true) { 164 requiresBound = bind 165 if (bind) { 166 if (wrapper == null) { 167 if (DEBUG) { 168 Log.d(TAG, "Binding service $intent") 169 } 170 try { 171 lastForPanel = forPanel 172 val flags = if (forPanel) BIND_FLAGS_PANEL else BIND_FLAGS 173 var bound = false 174 if (serviceConnection.connected.compareAndSet(false, true)) { 175 bound = context 176 .bindServiceAsUser(intent, serviceConnection, flags, user) 177 } 178 if (!bound) { 179 Log.d(TAG, "Couldn't bind to $intent") 180 doUnbind() 181 } 182 } catch (e: SecurityException) { 183 Log.e(TAG, "Failed to bind to service", e) 184 // Couldn't even bind. Let's reset the connected value 185 serviceConnection.connected.set(false) 186 } 187 } 188 } else { 189 unbindAndCleanup("unbind requested") 190 packageUpdateMonitor.stopMonitoring() 191 } 192 } 193 194 @WorkerThread unbindAndCleanupnull195 private fun unbindAndCleanup(reason: String) { 196 if (DEBUG) { 197 Log.d(TAG, "Unbinding service $intent. Reason: $reason") 198 } 199 wrapper = null 200 try { 201 doUnbind() 202 } catch (e: IllegalArgumentException) { 203 Log.e(TAG, "Failed to unbind service", e) 204 } 205 } 206 207 @WorkerThread doUnbindnull208 private fun doUnbind() { 209 if (serviceConnection.connected.compareAndSet(true, false)) { 210 context.unbindService(serviceConnection) 211 } 212 } 213 queueServiceMethodnull214 private fun queueServiceMethod(sm: ServiceMethod) { 215 synchronized(queuedServiceMethods) { 216 queuedServiceMethods.add(sm) 217 } 218 } 219 invokeOrQueuenull220 private fun invokeOrQueue(sm: ServiceMethod) { 221 wrapper?.run { 222 sm.run() 223 } ?: run { 224 queueServiceMethod(sm) 225 bindService(true) 226 } 227 } 228 229 /** 230 * Request a call to [IControlsProvider.load]. 231 * 232 * If the service is not bound, the call will be queued and the service will be bound first. 233 * The service will be unbound after the controls are returned or the call times out. 234 * 235 * @param subscriber the subscriber that manages coordination for loading controls 236 */ maybeBindAndLoadnull237 fun maybeBindAndLoad(subscriber: IControlsSubscriber.Stub) { 238 onLoadCanceller = executor.executeDelayed({ 239 // Didn't receive a response in time, log and send back error 240 Log.d(TAG, "Timeout waiting onLoad for $componentName") 241 subscriber.onError(token, "Timeout waiting onLoad") 242 unbindService() 243 }, LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS) 244 245 invokeOrQueue(Load(subscriber)) 246 } 247 248 /** 249 * Request a call to [IControlsProvider.loadSuggested]. 250 * 251 * If the service is not bound, the call will be queued and the service will be bound first. 252 * The service will be unbound if the call times out. 253 * 254 * @param subscriber the subscriber that manages coordination for loading controls 255 */ maybeBindAndLoadSuggestednull256 fun maybeBindAndLoadSuggested(subscriber: IControlsSubscriber.Stub) { 257 onLoadCanceller = executor.executeDelayed({ 258 // Didn't receive a response in time, log and send back error 259 Log.d(TAG, "Timeout waiting onLoadSuggested for $componentName") 260 subscriber.onError(token, "Timeout waiting onLoadSuggested") 261 unbindService() 262 }, LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS) 263 264 invokeOrQueue(Suggest(subscriber)) 265 } 266 cancelLoadTimeoutnull267 fun cancelLoadTimeout() { 268 onLoadCanceller?.run() 269 onLoadCanceller = null 270 } 271 272 /** 273 * Request a subscription to the [Publisher] returned by [ControlsProviderService.publisherFor] 274 * 275 * If the service is not bound, the call will be queued and the service will be bound first. 276 * 277 * @param controlIds a list of the ids of controls to send status back. 278 */ maybeBindAndSubscribenull279 fun maybeBindAndSubscribe(controlIds: List<String>, subscriber: IControlsSubscriber) = 280 invokeOrQueue(Subscribe(controlIds, subscriber)) 281 282 /** 283 * Request a call to [ControlsProviderService.performControlAction]. 284 * 285 * If the service is not bound, the call will be queued and the service will be bound first. 286 * 287 * @param controlId the id of the [Control] the action is performed on 288 * @param action the action performed 289 */ 290 fun maybeBindAndSendAction(controlId: String, action: ControlAction) = 291 invokeOrQueue(Action(controlId, action)) 292 293 /** 294 * Starts the subscription to the [ControlsProviderService] and requests status of controls. 295 * 296 * @param subscription the subscription to use to request controls 297 * @see maybeBindAndLoad 298 */ 299 fun startSubscription(subscription: IControlsSubscription, requestLimit: Long) { 300 if (DEBUG) { 301 Log.d(TAG, "startSubscription: $subscription") 302 } 303 304 wrapper?.request(subscription, requestLimit) 305 } 306 307 /** 308 * Cancels the subscription to the [ControlsProviderService]. 309 * 310 * @param subscription the subscription to cancel 311 * @see maybeBindAndLoad 312 */ cancelSubscriptionnull313 fun cancelSubscription(subscription: IControlsSubscription) { 314 if (DEBUG) { 315 Log.d(TAG, "cancelSubscription: $subscription") 316 } 317 318 wrapper?.cancel(subscription) 319 } 320 321 /** 322 * Request bind to the service. 323 */ bindServicenull324 fun bindService() { 325 bindService(true) 326 } 327 bindServiceForPanelnull328 fun bindServiceForPanel() { 329 bindService(bind = true, forPanel = true) 330 } 331 332 /** 333 * Request unbind from the service. 334 */ unbindServicenull335 fun unbindService() { 336 onLoadCanceller?.run() 337 onLoadCanceller = null 338 339 bindService(false) 340 } 341 toStringnull342 override fun toString(): String { 343 return StringBuilder("ControlsProviderLifecycleManager(").apply { 344 append("component=$componentName") 345 append(", user=$user") 346 append(")") 347 }.toString() 348 } 349 350 /** 351 * Service methods that can be queued or invoked, and are retryable for failure scenarios 352 */ 353 abstract inner class ServiceMethod { runnull354 fun run() { 355 if (!callWrapper()) { 356 queueServiceMethod(this) 357 executor.execute { unbindAndCleanup("couldn't call through binder") } 358 } 359 } 360 callWrappernull361 internal abstract fun callWrapper(): Boolean 362 } 363 364 inner class Load(val subscriber: IControlsSubscriber.Stub) : ServiceMethod() { 365 override fun callWrapper(): Boolean { 366 if (DEBUG) { 367 Log.d(TAG, "load $componentName") 368 } 369 return wrapper?.load(subscriber) ?: false 370 } 371 } 372 373 inner class Suggest(val subscriber: IControlsSubscriber.Stub) : ServiceMethod() { callWrappernull374 override fun callWrapper(): Boolean { 375 if (DEBUG) { 376 Log.d(TAG, "suggest $componentName") 377 } 378 return wrapper?.loadSuggested(subscriber) ?: false 379 } 380 } 381 inner class Subscribe( 382 val list: List<String>, 383 val subscriber: IControlsSubscriber 384 ) : ServiceMethod() { callWrappernull385 override fun callWrapper(): Boolean { 386 if (DEBUG) { 387 Log.d(TAG, "subscribe $componentName - $list") 388 } 389 390 return wrapper?.subscribe(list, subscriber) ?: false 391 } 392 } 393 394 inner class Action(val id: String, val action: ControlAction) : ServiceMethod() { callWrappernull395 override fun callWrapper(): Boolean { 396 if (DEBUG) { 397 Log.d(TAG, "onAction $componentName - $id") 398 } 399 return wrapper?.action(id, action, actionCallbackService) ?: false 400 } 401 } 402 } 403