Meshtastic-Android/app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt

132 lines
4.4 KiB
Kotlin

/*
* Copyright (c) 2024 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.android
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.IInterface
import com.geeksville.mesh.util.exceptionReporter
import java.io.Closeable
import java.lang.IllegalArgumentException
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class BindFailedException : Exception("bindService failed")
/**
* A wrapper that cleans up the service binding process
*/
open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T) : Closeable,
Logging {
var serviceP: T? = null
// A getter that returns the bound service or throws if not bound
val service: T
get() {
waitConnect() // Wait for at least the initial connection to happen
return serviceP ?: throw Exception("Service not bound")
}
private var context: Context? = null
private var isClosed = true
private val lock = ReentrantLock()
private val condition = lock.newCondition()
/** Call this if you want to stall until the connection is completed */
fun waitConnect() {
// Wait until this service is connected
lock.withLock {
if (context == null) {
throw Exception("Haven't called connect")
}
if (serviceP == null) {
condition.await()
}
}
}
fun connect(c: Context, intent: Intent, flags: Int) {
context = c
if (isClosed) {
isClosed = false
if (!c.bindService(intent, connection, flags)) {
// Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false. Try
// a short sleep to see if that helps
errormsg("Needed to use the second bind attempt hack")
Thread.sleep(500) // was 200ms, but received an autobug from a Galaxy Note4, android 6.0.1
if (!c.bindService(intent, connection, flags)) {
throw BindFailedException()
}
}
} else {
warn("Ignoring rebind attempt for service")
}
}
override fun close() {
isClosed = true
try {
context?.unbindService(connection)
} catch (ex: IllegalArgumentException) {
// Autobugs show this can generate an illegal arg exception for "service not registered" during reinstall?
warn("Ignoring error in ServiceClient.close, probably harmless")
}
serviceP = null
context = null
}
// Called when we become connected
open fun onConnected(service: T) {
}
// called on loss of connection
open fun onDisconnected() {
}
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) = exceptionReporter {
if (!isClosed) {
val s = stubFactory(binder)
serviceP = s
onConnected(s)
// after calling our handler, tell anyone who was waiting for this connection to complete
lock.withLock {
condition.signalAll()
}
} else {
// If we start to close a service, it seems that there is a possibility a onServiceConnected event is the queue
// for us. Be careful not to process that stale event
warn("A service connected while we were closing it, ignoring")
}
}
override fun onServiceDisconnected(name: ComponentName?) = exceptionReporter {
serviceP = null
onDisconnected()
}
}
}