UnifiedNlp/client/src/main/kotlin/org/microg/nlp/client/UnifiedLocationClient.kt

564 lines
20 KiB
Kotlin

/*
* SPDX-FileCopyrightText: 2019, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.client
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.location.Address
import android.location.Location
import android.os.Bundle
import android.os.DeadObjectException
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.*
import org.microg.nlp.service.AddressCallback
import org.microg.nlp.service.LocationCallback
import org.microg.nlp.service.UnifiedLocationService
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.*
private const val CALL_TIMEOUT = 10000L
@Deprecated("Use LocationClient or GeocodeClient")
class UnifiedLocationClient(private val context: Context, private val lifecycle: Lifecycle) : LifecycleOwner {
private var bound = false
private val serviceReferenceCount = AtomicInteger(0)
private val options = Bundle()
private var service: UnifiedLocationService? = null
private val syncThreads: ThreadPoolExecutor = ThreadPoolExecutor(1, Runtime.getRuntime().availableProcessors(), 1, TimeUnit.SECONDS, LinkedBlockingQueue())
private val waitingForService = arrayListOf<Continuation<UnifiedLocationService>>()
private var timer: Timer? = null
private var reconnectCount = 0
private val requests = CopyOnWriteArraySet<LocationRequest>()
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
this@UnifiedLocationClient.onServiceConnected(name, service)
}
override fun onServiceDisconnected(name: ComponentName) {
this@UnifiedLocationClient.onServiceDisconnected(name)
}
override fun onBindingDied(name: ComponentName) {
this@UnifiedLocationClient.onBindingDied(name)
}
override fun onNullBinding(name: ComponentName) {
this@UnifiedLocationClient.onNullBinding(name)
}
}
var forceNextUpdate: Boolean
get() = options.getBoolean(KEY_FORCE_NEXT_UPDATE, false)
set(value) = options.putBoolean(KEY_FORCE_NEXT_UPDATE, value)
var opPackageName: String?
get() = options.getString(KEY_OP_PACKAGE_NAME)
set(value) = options.putString(KEY_OP_PACKAGE_NAME, value)
val isAvailable: Boolean
get() = bound || resolve() != null
val targetPackage: String?
get() = resolve()?.`package`
private fun resolve(): Intent? = resolveIntent(context, ACTION_UNIFIED_LOCATION_SERVICE)
@Synchronized
private fun updateBinding(): Boolean {
Log.d(TAG, "updateBinding - current: $bound, refs: ${serviceReferenceCount.get()}, reqs: ${requests.size}, avail: $isAvailable")
if (!bound && (serviceReferenceCount.get() > 0 || !requests.isEmpty()) && isAvailable) {
timer = Timer("unified-client")
bound = true
bind()
return true
} else if (bound && serviceReferenceCount.get() == 0 && requests.isEmpty()) {
timer!!.cancel()
timer = null
bound = false
unbind()
return false
}
return bound
}
@Synchronized
private fun bindLater() {
val timer = timer
if (timer == null) {
updateBinding()
return
}
timer.schedule(object : TimerTask() {
override fun run() {
bind()
}
}, 1000)
}
@Synchronized
private fun bind() {
if (!bound) {
Log.w(TAG, "Tried to bind while not being bound!")
return
}
if (reconnectCount > 3) {
Log.w(TAG, "Reconnecting failed three times in a row, die out.")
return
}
val intent = resolve() ?: return
unbind()
reconnectCount++
Log.d(TAG, "Binding to $intent")
bound = context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
@Synchronized
private fun unbind() {
try {
this.context.unbindService(connection)
} catch (ignored: Exception) {
}
this.service = null
}
@Synchronized
fun ref() {
Log.d(TAG, "ref: ${Exception().stackTrace[1]}")
serviceReferenceCount.incrementAndGet()
updateBinding()
}
@Synchronized
fun unref() {
Log.d(TAG, "unref: ${Exception().stackTrace[1]}")
serviceReferenceCount.decrementAndGet()
updateBinding()
}
@Deprecated("Use LocationClient")
suspend fun getSingleLocation(): Location = suspendCoroutine { continuation ->
requestSingleLocation(LocationListener.wrap { continuation.resume(it) })
}
@Deprecated("Use LocationClient")
fun requestSingleLocation(listener: LocationListener) {
requestLocationUpdates(listener, 0, 1)
}
@Deprecated("Use LocationClient")
fun requestLocationUpdates(listener: LocationListener, interval: Long) {
requestLocationUpdates(listener, interval, Integer.MAX_VALUE)
}
@Deprecated("Use LocationClient")
fun requestLocationUpdates(listener: LocationListener, interval: Long, count: Int) {
requests.removeAll(requests.filter { it.listener === listener })
requests.add(LocationRequest(listener, interval, count))
lifecycleScope.launchWhenStarted {
updateServiceInterval()
updateBinding()
}
}
@Deprecated("Use LocationClient")
fun removeLocationUpdates(listener: LocationListener) {
lifecycleScope.launchWhenStarted {
removeRequests(requests.filter { it.listener === listener })
}
}
private suspend fun refAndGetService(): UnifiedLocationService = suspendCoroutine { continuation -> refAndGetServiceContinued(continuation) }
@Synchronized
private fun refAndGetServiceContinued(continuation: Continuation<UnifiedLocationService>) {
Log.d(TAG, "ref+get: ${Exception().stackTrace[2]}")
serviceReferenceCount.incrementAndGet()
waitForServiceContinued(continuation)
}
private suspend fun waitForService(): UnifiedLocationService = suspendCoroutine { continuation -> waitForServiceContinued(continuation) }
@Synchronized
private fun waitForServiceContinued(continuation: Continuation<UnifiedLocationService>) {
val service = service
if (service != null) {
continuation.resume(service)
} else {
synchronized(waitingForService) {
waitingForService.add(continuation)
}
updateBinding()
val timer = timer
if (timer == null) {
synchronized(waitingForService) {
waitingForService.remove(continuation)
}
continuation.resumeWithException(RuntimeException("No timer, called waitForService when not connected"))
} else {
timer.schedule(object : TimerTask() {
override fun run() {
try {
continuation.resumeWithException(TimeoutException())
} catch (e: IllegalStateException) {
// Resumed pretty much the same moment as timeout triggered, ignore
}
}
}, CALL_TIMEOUT)
}
}
}
private fun <T> configureContinuationTimeout(continuation: Continuation<T>, timeout: Long) {
if (timeout <= 0 || timeout == Long.MAX_VALUE) return
timer!!.schedule(object : TimerTask() {
override fun run() {
try {
Log.w(TAG, "Timeout reached")
continuation.resumeWithException(TimeoutException())
} catch (ignored: Exception) {
// Ignore
}
}
}, timeout)
}
private fun <T> executeSyncWithTimeout(timeout: Long = CALL_TIMEOUT, action: suspend () -> T): T {
var result: T? = null
val latch = CountDownLatch(1)
var err: Exception? = null
syncThreads.execute {
try {
runBlocking {
result = withTimeout(timeout) { action() }
}
} catch (e: Exception) {
err = e
} finally {
latch.countDown()
}
}
if (!latch.await(timeout, TimeUnit.MILLISECONDS))
throw TimeoutException()
err?.let { throw it }
return result ?: throw NullPointerException()
}
@Deprecated("Use GeocoderClient")
suspend fun getFromLocation(latitude: Double, longitude: Double, maxResults: Int, locale: String, timeout: Long = Long.MAX_VALUE): List<Address> {
try {
val service = refAndGetService()
return suspendCoroutine { continuation ->
service.getFromLocationWithOptions(latitude, longitude, maxResults, locale, options, AddressContinuation(continuation))
configureContinuationTimeout(continuation, timeout)
}
} catch (e: Exception) {
Log.w(TAG, "Failed to request geocode", e)
return emptyList()
} finally {
unref()
}
}
@Deprecated("Use GeocoderClient")
fun getFromLocationSync(latitude: Double, longitude: Double, maxResults: Int, locale: String, timeout: Long = CALL_TIMEOUT): List<Address> = executeSyncWithTimeout(timeout) {
getFromLocation(latitude, longitude, maxResults, locale, timeout)
}
@Deprecated("Use GeocoderClient")
suspend fun getFromLocationName(locationName: String, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String, timeout: Long = Long.MAX_VALUE): List<Address> {
return try {
val service = refAndGetService()
suspendCoroutine { continuation ->
service.getFromLocationNameWithOptions(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale, options, AddressContinuation(continuation))
configureContinuationTimeout(continuation, timeout)
}
} catch (e: Exception) {
Log.w(TAG, "Failed to request geocode", e)
emptyList()
} finally {
unref()
}
}
@Deprecated("Use GeocoderClient")
fun getFromLocationNameSync(locationName: String, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String, timeout: Long = CALL_TIMEOUT): List<Address> = executeSyncWithTimeout(timeout) {
getFromLocationName(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale, timeout)
}
@Deprecated("Use LocationClient")
suspend fun getLocationBackends(): Array<String> {
try {
return refAndGetService().locationBackends
} catch (e: Exception) {
Log.w(TAG, "Failed to handle request", e)
return emptyArray()
} finally {
unref()
}
}
@Deprecated("Use LocationClient")
suspend fun setLocationBackends(backends: Array<String>) {
try {
refAndGetService().locationBackends = backends
} catch (e: Exception) {
Log.w(TAG, "Failed to handle request", e)
} finally {
unref()
}
}
@Deprecated("Use GeocoderClient")
suspend fun getGeocoderBackends(): Array<String> {
try {
return refAndGetService().geocoderBackends
} catch (e: RemoteException) {
Log.w(TAG, "Failed to handle request", e)
return emptyArray()
} finally {
unref()
}
}
@Deprecated("Use GeocoderClient")
suspend fun setGeocoderBackends(backends: Array<String>) {
try {
refAndGetService().geocoderBackends = backends
} catch (e: RemoteException) {
Log.w(TAG, "Failed to handle request", e)
} finally {
unref()
}
}
@Deprecated("Use LocationClient")
fun getLastLocationSync(timeout: Long = CALL_TIMEOUT): Location? = executeSyncWithTimeout(timeout) {
getLastLocation()
}
@Deprecated("Use LocationClient")
suspend fun getLastLocation(): Location? {
return try {
refAndGetService().lastLocation
} catch (e: RemoteException) {
Log.w(TAG, "Failed to handle request", e)
null
} finally {
unref()
}
}
@Deprecated("Use LocationClient")
suspend fun getLastLocationForBackend(packageName: String, className: String, signatureDigest: String? = null): Location? {
return try {
refAndGetService().getLastLocationForBackend(packageName, className, signatureDigest)
} catch (e: RemoteException) {
Log.w(TAG, "Failed to handle request", e)
null
} finally {
unref()
}
}
private suspend fun removeRequestPendingRemoval() {
removeRequests(requests.filter { it.needsRemoval })
}
private suspend fun removeRequests(removalNeeded: List<LocationRequest>) {
if (removalNeeded.isNotEmpty()) {
requests.removeAll(removalNeeded)
updateServiceInterval()
updateBinding()
}
}
private suspend fun updateServiceInterval() {
var minTime = Long.MAX_VALUE
var requestSingle = false
for (request in requests) {
if (request.interval <= 0) {
requestSingle = true
forceNextUpdate = true
continue
}
if (request.interval <= minTime) {
minTime = request.interval
}
}
if (minTime == Long.MAX_VALUE) {
Log.d(TAG, "Disable automatic updates")
minTime = 0
} else {
Log.d(TAG, "Set update interval to $minTime")
}
val service = try {
waitForService()
} catch (e: Exception) {
Log.w(TAG, e)
return
}
try {
service.setUpdateInterval(minTime, options)
if (requestSingle) {
Log.d(TAG, "Request single update (force update: $forceNextUpdate)")
service.requestSingleUpdate(options)
forceNextUpdate = false
}
} catch (e: DeadObjectException) {
Log.w(TAG, "Connection is dead, reconnecting")
bind()
} catch (e: RemoteException) {
Log.w(TAG, "Failed to set location update interval", e)
}
}
@Synchronized
private fun onServiceConnected(name: ComponentName, binder: IBinder) {
Log.d(TAG, "Connected to $name")
reconnectCount = 0
val service = UnifiedLocationService.Stub.asInterface(binder)
this.service = service
val continuations = arrayListOf<Continuation<UnifiedLocationService>>()
synchronized(waitingForService) {
continuations.addAll(waitingForService)
waitingForService.clear()
}
lifecycleScope.launchWhenStarted {
try {
Log.d(TAG, "Registering location callback")
service.registerLocationCallback(object : LocationCallback.Stub() {
override fun onLocationUpdate(location: Location) {
lifecycleScope.launchWhenStarted {
this@UnifiedLocationClient.onLocationUpdate(location)
}
}
}, options)
Log.d(TAG, "Registered location callback")
} catch (e: Exception) {
Log.w(TAG, "Failed to register location callback", e)
}
updateServiceInterval()
if (continuations.size > 0) {
Log.d(TAG, "Resuming ${continuations.size} continuations")
}
for (continuation in continuations) {
try {
continuation.resume(service)
} catch (e: Exception) {
Log.w(TAG, e)
}
}
}
}
private suspend fun onLocationUpdate(location: Location) {
for (request in requests) {
request.handleLocation(location)
}
removeRequestPendingRemoval()
}
@Synchronized
private fun onServiceDisconnected(name: ComponentName) {
Log.d(TAG, "Disconnected from $name")
this.service = null
}
private fun onBindingDied(name: ComponentName) {
Log.d(TAG, "Connection to $name died, reconnecting")
bind()
}
private fun onNullBinding(name: ComponentName) {
Log.w(TAG, "Null binding from $name, reconnecting")
bindLater()
}
override fun getLifecycle(): Lifecycle = lifecycle
interface LocationListener {
fun onLocation(location: Location)
companion object {
fun wrap(listener: (Location) -> Unit): LocationListener {
return object : LocationListener {
override fun onLocation(location: Location) {
listener(location)
}
}
}
}
}
private inner class LocationRequest(val listener: LocationListener, var interval: Long, var pendingCount: Int) {
private var lastUpdate: Long = 0
private var failed: Boolean = false
val needsRemoval: Boolean
get() = pendingCount <= 0 || failed
fun reset(interval: Long, count: Int) {
this.interval = interval
this.pendingCount = count
}
@Synchronized
fun handleLocation(location: Location) {
if (needsRemoval) return
if (lastUpdate > System.currentTimeMillis()) {
lastUpdate = System.currentTimeMillis()
}
if (lastUpdate <= System.currentTimeMillis() - interval / 2) {
lastUpdate = System.currentTimeMillis()
if (pendingCount > 0) {
pendingCount--
}
try {
listener.onLocation(location)
} catch (e: Exception) {
Log.w(TAG, "Listener threw uncaught exception, stopping location request", e)
failed = true
}
}
}
}
companion object {
const val ACTION_UNIFIED_LOCATION_SERVICE = "org.microg.nlp.service.UnifiedLocationService"
const val PERMISSION_SERVICE_ADMIN = "org.microg.nlp.SERVICE_ADMIN"
const val KEY_FORCE_NEXT_UPDATE = "org.microg.nlp.FORCE_NEXT_UPDATE"
const val KEY_OP_PACKAGE_NAME = "org.microg.nlp.OP_PACKAGE_NAME"
private val TAG = "ULocClient"
}
}
class AddressContinuation(private val continuation: Continuation<List<Address>>) : AddressCallback.Stub() {
override fun onResult(addresses: List<Address>?) {
try {
if (addresses != null) {
Log.d("ULocClient", "Resume with ${addresses.size} addresses")
continuation.resume(addresses)
} else {
continuation.resumeWithException(NullPointerException("Service returned null"))
}
} catch (ignored: Exception) {
// Ignore
}
}
}