blob: a6af6a11d8b73e5897d544a0994b4c9ff325474a [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.controls.controller
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
import android.os.RemoteException
import android.os.UserHandle
import android.service.controls.ControlsProviderService
import android.service.controls.ControlsProviderService.CALLBACK_BUNDLE
import android.service.controls.ControlsProviderService.CALLBACK_TOKEN
import android.service.controls.IControlsActionCallback
import android.service.controls.IControlsProvider
import android.service.controls.IControlsSubscriber
import android.service.controls.IControlsSubscription
import android.service.controls.actions.ControlAction
import android.util.ArraySet
import android.util.Log
import com.android.internal.annotations.GuardedBy
import com.android.systemui.util.concurrency.DelayableExecutor
import java.util.concurrent.TimeUnit
/**
* Manager for the lifecycle of the connection to a given [ControlsProviderService].
*
* This class handles binding and unbinding and requests to the service. The class will queue
* requests until the service is connected and dispatch them then.
*
* @property context A SystemUI context for binding to the services
* @property executor A delayable executor for posting timeouts
* @property actionCallbackService a callback interface to hand the remote service for sending
* action responses
* @property subscriberService an "subscriber" interface for requesting and accepting updates for
* controls from the service.
* @property user the user for whose this service should be bound.
* @property componentName the name of the component for the service.
*/
class ControlsProviderLifecycleManager(
private val context: Context,
private val executor: DelayableExecutor,
private val actionCallbackService: IControlsActionCallback.Stub,
val user: UserHandle,
val componentName: ComponentName
) : IBinder.DeathRecipient {
val token: IBinder = Binder()
private var requiresBound = false
@GuardedBy("queuedServiceMethods")
private val queuedServiceMethods: MutableSet<ServiceMethod> = ArraySet()
private var wrapper: ServiceWrapper? = null
private var bindTryCount = 0
private val TAG = javaClass.simpleName
private var onLoadCanceller: Runnable? = null
companion object {
private const val BIND_RETRY_DELAY = 1000L // ms
private const val LOAD_TIMEOUT_SECONDS = 30L // seconds
private const val MAX_BIND_RETRIES = 5
private const val DEBUG = true
private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE
}
private val intent = Intent().apply {
component = componentName
putExtra(CALLBACK_BUNDLE, Bundle().apply {
putBinder(CALLBACK_TOKEN, token)
})
}
private fun bindService(bind: Boolean) {
executor.execute {
requiresBound = bind
if (bind) {
if (bindTryCount != MAX_BIND_RETRIES) {
if (DEBUG) {
Log.d(TAG, "Binding service $intent")
}
bindTryCount++
try {
context.bindServiceAsUser(intent, serviceConnection, BIND_FLAGS, user)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind to service", e)
}
}
} else {
if (DEBUG) {
Log.d(TAG, "Unbinding service $intent")
}
bindTryCount = 0
wrapper?.run {
context.unbindService(serviceConnection)
}
wrapper = null
}
}
}
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
if (DEBUG) Log.d(TAG, "onServiceConnected $name")
bindTryCount = 0
wrapper = ServiceWrapper(IControlsProvider.Stub.asInterface(service))
try {
service.linkToDeath(this@ControlsProviderLifecycleManager, 0)
} catch (_: RemoteException) {}
handlePendingServiceMethods()
}
override fun onServiceDisconnected(name: ComponentName?) {
if (DEBUG) Log.d(TAG, "onServiceDisconnected $name")
wrapper = null
bindService(false)
}
}
private fun handlePendingServiceMethods() {
val queue = synchronized(queuedServiceMethods) {
ArraySet(queuedServiceMethods).also {
queuedServiceMethods.clear()
}
}
queue.forEach {
it.run()
}
}
override fun binderDied() {
if (wrapper == null) return
wrapper = null
if (requiresBound) {
if (DEBUG) {
Log.d(TAG, "binderDied")
}
// Try rebinding some time later
}
}
private fun queueServiceMethod(sm: ServiceMethod) {
synchronized(queuedServiceMethods) {
queuedServiceMethods.add(sm)
}
}
private fun invokeOrQueue(sm: ServiceMethod) {
wrapper?.run {
sm.run()
} ?: run {
queueServiceMethod(sm)
bindService(true)
}
}
/**
* Request a call to [IControlsProvider.load].
*
* If the service is not bound, the call will be queued and the service will be bound first.
* The service will be unbound after the controls are returned or the call times out.
*
* @param subscriber the subscriber that manages coordination for loading controls
*/
fun maybeBindAndLoad(subscriber: IControlsSubscriber.Stub) {
onLoadCanceller = executor.executeDelayed({
// Didn't receive a response in time, log and send back error
Log.d(TAG, "Timeout waiting onLoad for $componentName")
subscriber.onError(token, "Timeout waiting onLoad")
unbindService()
}, LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
invokeOrQueue(Load(subscriber))
}
/**
* Request a call to [IControlsProvider.loadSuggested].
*
* If the service is not bound, the call will be queued and the service will be bound first.
* The service will be unbound if the call times out.
*
* @param subscriber the subscriber that manages coordination for loading controls
*/
fun maybeBindAndLoadSuggested(subscriber: IControlsSubscriber.Stub) {
onLoadCanceller = executor.executeDelayed({
// Didn't receive a response in time, log and send back error
Log.d(TAG, "Timeout waiting onLoadSuggested for $componentName")
subscriber.onError(token, "Timeout waiting onLoadSuggested")
unbindService()
}, LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
invokeOrQueue(Suggest(subscriber))
}
fun cancelLoadTimeout() {
onLoadCanceller?.run()
onLoadCanceller = null
}
/**
* Request a subscription to the [Publisher] returned by [ControlsProviderService.publisherFor]
*
* If the service is not bound, the call will be queued and the service will be bound first.
*
* @param controlIds a list of the ids of controls to send status back.
*/
fun maybeBindAndSubscribe(controlIds: List<String>, subscriber: IControlsSubscriber) =
invokeOrQueue(Subscribe(controlIds, subscriber))
/**
* Request a call to [ControlsProviderService.performControlAction].
*
* If the service is not bound, the call will be queued and the service will be bound first.
*
* @param controlId the id of the [Control] the action is performed on
* @param action the action performed
*/
fun maybeBindAndSendAction(controlId: String, action: ControlAction) =
invokeOrQueue(Action(controlId, action))
/**
* Starts the subscription to the [ControlsProviderService] and requests status of controls.
*
* @param subscription the subscription to use to request controls
* @see maybeBindAndLoad
*/
fun startSubscription(subscription: IControlsSubscription, requestLimit: Long) {
if (DEBUG) {
Log.d(TAG, "startSubscription: $subscription")
}
wrapper?.request(subscription, requestLimit)
}
/**
* Cancels the subscription to the [ControlsProviderService].
*
* @param subscription the subscription to cancel
* @see maybeBindAndLoad
*/
fun cancelSubscription(subscription: IControlsSubscription) {
if (DEBUG) {
Log.d(TAG, "cancelSubscription: $subscription")
}
wrapper?.cancel(subscription)
}
/**
* Request bind to the service.
*/
fun bindService() {
bindService(true)
}
/**
* Request unbind from the service.
*/
fun unbindService() {
onLoadCanceller?.run()
onLoadCanceller = null
bindService(false)
}
override fun toString(): String {
return StringBuilder("ControlsProviderLifecycleManager(").apply {
append("component=$componentName")
append(", user=$user")
append(")")
}.toString()
}
/**
* Service methods that can be queued or invoked, and are retryable for failure scenarios
*/
abstract inner class ServiceMethod {
fun run() {
if (!callWrapper()) {
queueServiceMethod(this)
binderDied()
}
}
internal abstract fun callWrapper(): Boolean
}
inner class Load(val subscriber: IControlsSubscriber.Stub) : ServiceMethod() {
override fun callWrapper(): Boolean {
if (DEBUG) {
Log.d(TAG, "load $componentName")
}
return wrapper?.load(subscriber) ?: false
}
}
inner class Suggest(val subscriber: IControlsSubscriber.Stub) : ServiceMethod() {
override fun callWrapper(): Boolean {
if (DEBUG) {
Log.d(TAG, "suggest $componentName")
}
return wrapper?.loadSuggested(subscriber) ?: false
}
}
inner class Subscribe(
val list: List<String>,
val subscriber: IControlsSubscriber
) : ServiceMethod() {
override fun callWrapper(): Boolean {
if (DEBUG) {
Log.d(TAG, "subscribe $componentName - $list")
}
return wrapper?.subscribe(list, subscriber) ?: false
}
}
inner class Action(val id: String, val action: ControlAction) : ServiceMethod() {
override fun callWrapper(): Boolean {
if (DEBUG) {
Log.d(TAG, "onAction $componentName - $id")
}
return wrapper?.action(id, action, actionCallbackService) ?: false
}
}
}