Add client

This commit is contained in:
Marvin W 2020-01-24 23:07:08 +01:00
parent aff82ab98c
commit 8e1a752024
No known key found for this signature in database
GPG key ID: 072E9235DB996F2A
7 changed files with 716 additions and 0 deletions

74
client/build.gradle Normal file
View file

@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: 2019, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'maven-publish'
android {
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
repositories {
mavenCentral()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
}
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
pom {
name = 'UnifiedNlp Client'
description = 'UnifiedNlp client library'
url = 'https://github.com/microg/UnifiedNlp'
licenses {
license {
name = 'The Apache Software License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id = 'microg'
name = 'microG Team'
}
developer {
id = 'mar-v-in'
name = 'Marvin W.'
}
}
scm {
url = 'https://github.com/microg/UnifiedNlp'
connection = 'scm:git:https://github.com/microg/UnifiedNlp.git'
developerConnection = 'scm:git:ssh://github.com/microg/UnifiedNlp.git'
}
}
from components.release
}
}
}
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2019, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.microg.nlp.client">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application />
</manifest>

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.service;
import android.location.Location;
interface AddressCallback {
void onResult(in List<Address> location);
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2019, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.service;
import android.location.Location;
interface LocationCallback {
void onLocationUpdate(in Location location);
}

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2019, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.service;
import android.location.Location;
import org.microg.nlp.service.AddressCallback;
import org.microg.nlp.service.LocationCallback;
interface UnifiedLocationService {
void registerLocationCallback(LocationCallback callback, in Bundle options);
void setUpdateInterval(long interval, in Bundle options);
void requestSingleUpdate(in Bundle options);
void getFromLocationWithOptions(double latitude, double longitude, int maxResults,
String locale, in Bundle options, AddressCallback callback);
void getFromLocationNameWithOptions(String locationName, int maxResults,
double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude,
double upperRightLongitude, String locale, in Bundle options, AddressCallback callback);
void reloadPreferences();
String[] getLocationBackends();
void setLocationBackends(in String[] backends);
String[] getGeocoderBackends();
void setGeocoderBackends(in String[] backends);
Location getLastLocation();
Location getLastLocationForBackend(String packageName, String className, String signatureDigest);
}

View file

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: 2008, The Android Open Source Project
* SPDX-License-Identifier: Apache-2.0
*/
package android.location;
parcelable Address;

View file

@ -0,0 +1,567 @@
/*
* 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.content.pm.ApplicationInfo
import android.content.pm.ResolveInfo
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 org.microg.nlp.service.AddressCallback
import org.microg.nlp.service.LocationCallback
import org.microg.nlp.service.UnifiedLocationService
import java.lang.ref.WeakReference
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.atomic.AtomicInteger
import android.content.pm.PackageManager.SIGNATURE_MATCH
import kotlinx.coroutines.*
import java.util.concurrent.*
import kotlin.coroutines.*
import kotlin.math.min
private const val CALL_TIMEOUT = 10000L
class UnifiedLocationClient private constructor(context: Context) {
private var bound = false
private val serviceReferenceCount = AtomicInteger(0)
private val options = Bundle()
private var context: WeakReference<Context> = WeakReference(context)
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)
val isAvailable: Boolean
get() = bound || resolve() != null
val targetPackage: String?
get() = resolve()?.`package`
private fun resolve(): Intent? {
val context = this.context.get() ?: return null
val intent = Intent(ACTION_UNIFIED_LOCATION_SERVICE)
val pm = context.packageManager
var resolveInfos = pm.queryIntentServices(intent, 0)
if (resolveInfos.size > 1) {
// Pick own package if possible
for (info in resolveInfos) {
if (info.serviceInfo.packageName == context.packageName) {
intent.setPackage(info.serviceInfo.packageName)
Log.d(TAG, "Found more than one active unified service, picked own package " + intent.getPackage()!!)
return intent
}
}
// Pick package with matching signature if possible
for (info in resolveInfos) {
if (pm.checkSignatures(context.packageName, info.serviceInfo.packageName) == SIGNATURE_MATCH) {
intent.setPackage(info.serviceInfo.packageName)
Log.d(TAG, "Found more than one active unified service, picked related package " + intent.getPackage()!!)
return intent
}
}
// Restrict to system if single package is system
if (resolveInfos.any {
(it.serviceInfo?.applicationInfo?.flags
?: 0) and ApplicationInfo.FLAG_SYSTEM > 0
}) {
Log.d(TAG, "Found more than one active unified service, restricted to system packages")
resolveInfos = resolveInfos.filter {
(it.serviceInfo?.applicationInfo?.flags
?: 0) and ApplicationInfo.FLAG_SYSTEM > 0
}
}
var highestPriority: ResolveInfo? = null
for (info in resolveInfos) {
if (highestPriority == null || highestPriority.priority < info.priority) {
highestPriority = info
}
}
intent.setPackage(highestPriority!!.serviceInfo.packageName)
Log.d(TAG, "Found more than one active unified service, picked highest priority " + intent.getPackage()!!)
return intent
} else if (!resolveInfos.isEmpty()) {
intent.setPackage(resolveInfos[0].serviceInfo.packageName)
return intent
} else {
Log.w(TAG, "No service to bind to, your system does not support unified service")
return null
}
}
@Synchronized
private fun updateBinding() {
if (!bound && (serviceReferenceCount.get() > 0 || !requests.isEmpty()) && isAvailable) {
timer = Timer("unified-client")
bound = true
bind()
} else if (bound && serviceReferenceCount.get() == 0 && requests.isEmpty()) {
timer!!.cancel()
timer = null
bound = false
unbind()
}
}
@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() {
val context = this.context.get() ?: return
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++
bound = context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
@Synchronized
private fun unbind() {
try {
this.context.get()?.let {
it.unbindService(connection)
}
} catch (ignored: Exception) {
}
this.service = null
}
fun ref() {
serviceReferenceCount.incrementAndGet()
updateBinding()
}
fun unref() {
serviceReferenceCount.decrementAndGet()
updateBinding()
}
suspend fun getSingleLocation(): Location = suspendCoroutine { continuation ->
requestSingleLocation(LocationListener.wrap { continuation.resume(it) })
}
fun requestSingleLocation(listener: LocationListener) {
requestLocationUpdates(listener, 0, 1)
}
fun requestLocationUpdates(listener: LocationListener, interval: Long) {
requestLocationUpdates(listener, interval, Integer.MAX_VALUE)
}
@Synchronized
fun requestLocationUpdates(listener: LocationListener, interval: Long, count: Int) {
for (request in requests) {
if (request.listener === listener) {
requests.remove(request)
}
}
requests.add(LocationRequest(listener, interval, count))
updateServiceInterval()
updateBinding()
}
@Synchronized
fun removeLocationUpdates(listener: LocationListener) {
for (request in requests) {
if (request.listener === listener) {
requests.remove(request)
}
}
updateServiceInterval()
updateBinding()
}
private suspend fun refAndGetService(): UnifiedLocationService = suspendCoroutine { continuation -> refAndGetServiceContinued(continuation) }
@Synchronized
private fun refAndGetServiceContinued(continuation: Continuation<UnifiedLocationService>) {
serviceReferenceCount.incrementAndGet()
val service = service
if (service != null) {
continuation.resume(service)
} else {
synchronized(waitingForService) {
waitingForService.add(continuation)
}
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)
updateBinding()
}
}
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)
}
fun <T> executeSyncWithTimeout(timeout: Long = CALL_TIMEOUT, action: suspend () -> T): T {
var result: T? = null
val latch = CountDownLatch(1)
syncThreads.execute {
runBlocking {
result = withTimeout(timeout) { action() }
latch.countDown()
}
}
if (!latch.await(timeout, TimeUnit.MILLISECONDS))
throw TimeoutException()
return result ?: throw NullPointerException()
}
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: RemoteException) {
Log.w(TAG, "Failed to request geocode", e)
return emptyList()
} finally {
unref()
}
}
fun getFromLocationSync(latitude: Double, longitude: Double, maxResults: Int, locale: String, timeout: Long = CALL_TIMEOUT): List<Address> = executeSyncWithTimeout(timeout) {
getFromLocation(latitude, longitude, maxResults, locale, timeout)
}
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: RemoteException) {
Log.w(TAG, "Failed to request geocode", e)
emptyList()
} finally {
unref()
}
}
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)
}
suspend fun reloadPreferences() {
try {
refAndGetService().reloadPreferences()
} catch (e: RemoteException) {
Log.w(TAG, "Failed to handle request", e)
} finally {
unref()
}
}
suspend fun getLocationBackends(): Array<String> {
try {
return refAndGetService().locationBackends
} catch (e: RemoteException) {
Log.w(TAG, "Failed to handle request", e)
return emptyArray()
} finally {
unref()
}
}
suspend fun setLocationBackends(backends: Array<String>) {
try {
refAndGetService().locationBackends = backends
} catch (e: RemoteException) {
Log.w(TAG, "Failed to handle request", e)
} finally {
unref()
}
}
suspend fun getGeocoderBackends(): Array<String> {
try {
return refAndGetService().geocoderBackends
} catch (e: RemoteException) {
Log.w(TAG, "Failed to handle request", e)
return emptyArray()
} finally {
unref()
}
}
suspend fun setGeocoderBackends(backends: Array<String>) {
try {
refAndGetService().locationBackends = backends
} catch (e: RemoteException) {
Log.w(TAG, "Failed to handle request", e)
} finally {
unref()
}
}
suspend fun getLastLocation(): Location? {
return try {
refAndGetService().lastLocation
} catch (e: RemoteException) {
Log.w(TAG, "Failed to handle request", e)
null
} finally {
unref()
}
}
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()
}
}
@Synchronized
private fun removeRequest(request: LocationRequest) {
requests.remove(request)
updateServiceInterval()
updateBinding()
}
@Synchronized
private fun updateServiceInterval() {
if (service == null) return
var interval: Long = Long.MAX_VALUE
var requestSingle = false
for (request in requests) {
if (request.interval <= 0) {
requestSingle = true
continue
}
interval = min(interval, request.interval)
}
if (interval == Long.MAX_VALUE) {
Log.d(TAG, "Disable automatic updates")
interval = 0
} else {
Log.d(TAG, "Set update interval to $interval")
}
try {
service!!.setUpdateInterval(interval, 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()
}
for (continuation in continuations) {
continuation.resume(service)
}
try {
service.registerLocationCallback(object : LocationCallback.Stub() {
override fun onLocationUpdate(location: Location) {
for (request in requests) {
request.handleLocation(location)
}
}
}, options)
updateServiceInterval()
} catch (e: Exception) {
Log.w(TAG, "Failed to register location callback", e)
}
}
@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()
}
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
fun reset(interval: Long, count: Int) {
this.interval = interval
this.pendingCount = count
}
@Synchronized
fun handleLocation(location: Location) {
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)
removeRequest(this)
}
}
if (pendingCount == 0) {
removeRequest(this)
}
}
}
companion object {
const val ACTION_UNIFIED_LOCATION_SERVICE = "org.microg.nlp.service.UnifiedLocationService"
const val KEY_FORCE_NEXT_UPDATE = "org.microg.nlp.client.FORCE_NEXT_UPDATE"
private val TAG = "ULocClient"
private var client: UnifiedLocationClient? = null
@JvmStatic
@Synchronized
operator fun get(context: Context): UnifiedLocationClient {
var client = client
if (client == null) {
client = UnifiedLocationClient(context)
this.client = client
} else {
client.context = WeakReference(context)
}
return client
}
}
}
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
}
}
}