blob: 79057ad25b206b57e596bafbe3f9d3ef960dbc47 [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.service.controls.Control
import android.service.controls.ControlsProviderService.CALLBACK_BINDER
import android.service.controls.ControlsProviderService.CALLBACK_BUNDLE
import android.service.controls.ControlsProviderService.CALLBACK_TOKEN
import android.service.controls.IControlsProvider
import android.service.controls.IControlsProviderCallback
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
typealias LoadCallback = (List<Control>) -> Unit
class ControlsProviderLifecycleManager(
private val context: Context,
private val executor: DelayableExecutor,
private val serviceCallback: IControlsProviderCallback.Stub,
val componentName: ComponentName
) : IBinder.DeathRecipient {
var lastLoadCallback: LoadCallback? = null
private set
val token: IBinder = Binder()
private var unbindImmediate = false
private var requiresBound = false
private var isBound = false
@GuardedBy("queuedMessages")
private val queuedMessages: MutableSet<Message> = ArraySet()
private var wrapper: ControlsProviderServiceWrapper? = null
private var bindTryCount = 0
private val TAG = javaClass.simpleName
private var onLoadCanceller: Runnable? = null
companion object {
private const val MSG_LOAD = 0
private const val MSG_SUBSCRIBE = 1
private const val MSG_UNSUBSCRIBE = 2
private const val MSG_ON_ACTION = 3
private const val MSG_UNBIND = 4
private const val BIND_RETRY_DELAY = 1000L // ms
private const val LOAD_TIMEOUT = 5000L // ms
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 or
Context.BIND_WAIVE_PRIORITY
}
private val intent = Intent().apply {
component = componentName
putExtra(CALLBACK_BUNDLE, Bundle().apply {
putBinder(CALLBACK_BINDER, serviceCallback)
putBinder(CALLBACK_TOKEN, token)
})
}
private fun bindService(bind: Boolean) {
requiresBound = bind
if (bind) {
if (bindTryCount == MAX_BIND_RETRIES) {
return
}
if (DEBUG) {
Log.d(TAG, "Binding service $intent")
}
bindTryCount++
try {
isBound = context.bindService(intent, serviceConnection, BIND_FLAGS)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind to service", e)
isBound = false
}
} else {
if (DEBUG) {
Log.d(TAG, "Unbinding service $intent")
}
bindTryCount = 0
wrapper = null
if (isBound) {
context.unbindService(serviceConnection)
isBound = false
}
}
}
fun bindPermanently() {
unbindImmediate = false
unqueueMessage(Message.Unbind)
bindService(true)
}
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
if (DEBUG) Log.d(TAG, "onServiceConnected $name")
bindTryCount = 0
wrapper = ControlsProviderServiceWrapper(IControlsProvider.Stub.asInterface(service))
try {
service.linkToDeath(this@ControlsProviderLifecycleManager, 0)
} catch (_: RemoteException) {}
handlePendingMessages()
}
override fun onServiceDisconnected(name: ComponentName?) {
if (DEBUG) Log.d(TAG, "onServiceDisconnected $name")
isBound = false
bindService(false)
}
}
private fun handlePendingMessages() {
val queue = synchronized(queuedMessages) {
ArraySet(queuedMessages).also {
queuedMessages.clear()
}
}
if (Message.Unbind in queue) {
bindService(false)
return
}
if (Message.Load in queue) {
load()
}
queue.filter { it is Message.Subscribe }.flatMap { (it as Message.Subscribe).list }.run {
subscribe(this)
}
queue.filter { it is Message.Action }.forEach {
val msg = it as Message.Action
onAction(msg.id, msg.action)
}
}
override fun binderDied() {
if (wrapper == null) return
wrapper = null
if (requiresBound) {
if (DEBUG) {
Log.d(TAG, "binderDied")
}
// Try rebinding some time later
}
}
private fun queueMessage(message: Message) {
synchronized(queuedMessages) {
queuedMessages.add(message)
}
}
private fun unqueueMessage(message: Message) {
synchronized(queuedMessages) {
queuedMessages.removeIf { it.type == message.type }
}
}
private fun load() {
if (DEBUG) {
Log.d(TAG, "load $componentName")
}
if (!(wrapper?.load() ?: false)) {
queueMessage(Message.Load)
binderDied()
}
}
fun maybeBindAndLoad(callback: LoadCallback) {
unqueueMessage(Message.Unbind)
lastLoadCallback = callback
onLoadCanceller = executor.executeDelayed({
// Didn't receive a response in time, log and send back empty list
Log.d(TAG, "Timeout waiting onLoad for $componentName")
serviceCallback.onLoad(token, emptyList())
}, LOAD_TIMEOUT, TimeUnit.MILLISECONDS)
if (isBound) {
load()
} else {
queueMessage(Message.Load)
unbindImmediate = true
bindService(true)
}
}
fun maybeBindAndSubscribe(controlIds: List<String>) {
if (isBound) {
subscribe(controlIds)
} else {
queueMessage(Message.Subscribe(controlIds))
bindService(true)
}
}
private fun subscribe(controlIds: List<String>) {
if (DEBUG) {
Log.d(TAG, "subscribe $componentName - $controlIds")
}
if (!(wrapper?.subscribe(controlIds) ?: false)) {
queueMessage(Message.Subscribe(controlIds))
binderDied()
}
}
fun maybeBindAndSendAction(controlId: String, action: ControlAction) {
if (isBound) {
onAction(controlId, action)
} else {
queueMessage(Message.Action(controlId, action))
bindService(true)
}
}
private fun onAction(controlId: String, action: ControlAction) {
if (DEBUG) {
Log.d(TAG, "onAction $componentName - $controlId")
}
if (!(wrapper?.onAction(controlId, action) ?: false)) {
queueMessage(Message.Action(controlId, action))
binderDied()
}
}
fun unsubscribe() {
if (DEBUG) {
Log.d(TAG, "unsubscribe $componentName")
}
unqueueMessage(Message.Subscribe(emptyList())) // Removes all subscribe messages
wrapper?.unsubscribe()
}
fun maybeUnbindAndRemoveCallback() {
lastLoadCallback = null
onLoadCanceller?.run()
onLoadCanceller = null
if (unbindImmediate) {
bindService(false)
}
}
fun unbindService() {
unbindImmediate = true
maybeUnbindAndRemoveCallback()
}
sealed class Message {
abstract val type: Int
object Load : Message() {
override val type = MSG_LOAD
}
object Unbind : Message() {
override val type = MSG_UNBIND
}
class Subscribe(val list: List<String>) : Message() {
override val type = MSG_SUBSCRIBE
}
class Action(val id: String, val action: ControlAction) : Message() {
override val type = MSG_ON_ACTION
}
}
}