From 3845b43f07219af01100c6affa94d8bc3dff491a Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 18 Jan 2022 13:46:39 +0100 Subject: [PATCH] Adjust code for new service API --- client/build.gradle | 4 +- .../org/microg/nlp/client/BaseClient.kt | 166 ++++++++++++++++++ .../org/microg/nlp/client/GeocodeClient.kt | 93 ++++++++++ .../org/microg/nlp/client/IntentResolver.kt | 62 +++++++ .../org/microg/nlp/client/LocationClient.kt | 103 +++++++++++ .../nlp/client/UnifiedLocationClient.kt | 150 +++++----------- compat/build.gradle | 14 +- geocode-v1/build.gradle | 6 + .../nlp/geocode/v1/GeocodeProvider.java | 59 ------- .../microg/nlp/geocode/v1/GeocodeProvider.kt | 64 +++++++ .../microg/nlp/geocode/v1/GeocodeService.java | 33 ---- .../microg/nlp/geocode/v1/GeocodeService.kt | 43 +++++ location-v2/build.gradle | 6 + .../nlp/location/v2/LocationProvider.java | 99 ----------- .../nlp/location/v2/LocationProvider.kt | 149 ++++++++++++++++ .../nlp/location/v2/LocationService.java | 33 ---- .../microg/nlp/location/v2/LocationService.kt | 48 +++++ location-v3/build.gradle | 6 + .../nlp/location/v3/LocationService.java | 9 - .../microg/nlp/location/v3/LocationService.kt | 9 + ui/build.gradle | 5 +- .../org/microg/nlp/ui/BackendConfiguration.kt | 31 +++- .../microg/nlp/ui/BackendDetailsFragment.kt | 35 ++-- .../org/microg/nlp/ui/BackendListFragment.kt | 16 +- 24 files changed, 863 insertions(+), 380 deletions(-) create mode 100644 client/src/main/kotlin/org/microg/nlp/client/BaseClient.kt create mode 100644 client/src/main/kotlin/org/microg/nlp/client/GeocodeClient.kt create mode 100644 client/src/main/kotlin/org/microg/nlp/client/IntentResolver.kt create mode 100644 client/src/main/kotlin/org/microg/nlp/client/LocationClient.kt delete mode 100644 geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeProvider.java create mode 100644 geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeProvider.kt delete mode 100644 geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeService.java create mode 100644 geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeService.kt delete mode 100644 location-v2/src/main/java/org/microg/nlp/location/v2/LocationProvider.java create mode 100644 location-v2/src/main/java/org/microg/nlp/location/v2/LocationProvider.kt delete mode 100644 location-v2/src/main/java/org/microg/nlp/location/v2/LocationService.java create mode 100644 location-v2/src/main/java/org/microg/nlp/location/v2/LocationService.kt delete mode 100644 location-v3/src/main/java/org/microg/nlp/location/v3/LocationService.java create mode 100644 location-v3/src/main/java/org/microg/nlp/location/v3/LocationService.kt diff --git a/client/build.gradle b/client/build.gradle index 3eb26bc..4ffa331 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -5,7 +5,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'maven-publish' apply plugin: 'signing' @@ -34,6 +33,9 @@ apply from: '../gradle/publish.gradle' description = 'UnifiedNlp client library' dependencies { + api project(":service-api") + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" } diff --git a/client/src/main/kotlin/org/microg/nlp/client/BaseClient.kt b/client/src/main/kotlin/org/microg/nlp/client/BaseClient.kt new file mode 100644 index 0000000..2ac3c54 --- /dev/null +++ b/client/src/main/kotlin/org/microg/nlp/client/BaseClient.kt @@ -0,0 +1,166 @@ +/* + * SPDX-FileCopyrightText: 2020, 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.Context.BIND_ABOVE_CLIENT +import android.content.Context.BIND_AUTO_CREATE +import android.content.Intent +import android.content.ServiceConnection +import android.location.Location +import android.os.Bundle +import android.os.IBinder +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.microg.nlp.service.api.Constants +import org.microg.nlp.service.api.ILocationListener +import org.microg.nlp.service.api.IStatusCallback +import org.microg.nlp.service.api.IStringsCallback +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +private const val TAG = "BaseClient" + +abstract class BaseClient(val context: Context, private val lifecycle: Lifecycle, val asInterface: (IBinder) -> I) : LifecycleOwner { + private val callbackThreads: ThreadPoolExecutor = ThreadPoolExecutor(1, Runtime.getRuntime().availableProcessors(), 1, TimeUnit.SECONDS, LinkedBlockingQueue()) + private var persistedConnectionCounter = AtomicInteger(0) + private val serviceConnectionMutex = Mutex() + private var persistedServiceConnection: ContinuedServiceConnection? = null + val defaultOptions = Bundle() + + abstract val action: String + + val intent: Intent? + get() = resolveIntent(context, action) + + val isAvailable: Boolean + get() = intent != null + + var packageName: String + get() = defaultOptions.getString("packageName") ?: context.packageName + set(value) = defaultOptions.putString("packageName", value) + + init { + packageName = context.packageName + } + + val isConnectedUnsafe: Boolean + get() = persistedServiceConnection != null && persistedConnectionCounter.get() > 0 + + suspend fun isConnected(): Boolean = serviceConnectionMutex.withLock { + return persistedServiceConnection != null && persistedConnectionCounter.get() > 0 + } + + suspend fun connect() { + serviceConnectionMutex.withLock { + if (persistedServiceConnection == null) { + val intent = intent ?: throw IllegalStateException("$action service is not available") + persistedServiceConnection = suspendCoroutine { continuation -> + context.bindService(intent, ContinuedServiceConnection(continuation), BIND_AUTO_CREATE or BIND_ABOVE_CLIENT) + } + } + persistedConnectionCounter.incrementAndGet() + } + } + + suspend fun disconnect() { + serviceConnectionMutex.withLock { + if (persistedConnectionCounter.decrementAndGet() <= 0) { + persistedServiceConnection?.let { context.unbindService(it) } + persistedServiceConnection = null + persistedConnectionCounter.set(0) + } + } + } + + fun withConnectedServiceSync(v: (I) -> T): T { + try { + if (persistedConnectionCounter.incrementAndGet() <= 1) { + throw IllegalStateException("Service not connected") + } + val service = persistedServiceConnection?.service ?: throw RuntimeException("No binder returned") + return v(asInterface(service)) + } finally { + persistedConnectionCounter.decrementAndGet() + } + } + + suspend fun withService(v: suspend (I) -> T): T { + connect() + val service = persistedServiceConnection?.service ?: throw RuntimeException("No binder returned") + return try { + v(asInterface(service)) + } finally { + disconnect() + } + } + + override fun getLifecycle(): Lifecycle = lifecycle +} + +internal class ContinuedServiceConnection(private val continuation: Continuation) : ServiceConnection { + var service: IBinder? = null + private var continued: Boolean = false + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + this.service = service + if (!continued) { + continued = true + continuation.resume(this) + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + if (!continued) { + continued = true + continuation.resumeWithException(RuntimeException("Disconnected")) + } + } + + override fun onBindingDied(name: ComponentName?) { + if (!continued) { + continued = true + continuation.resumeWithException(RuntimeException("Binding diead")) + } + } + + override fun onNullBinding(name: ComponentName?) { + if (!continued) { + continued = true + continuation.resume(this) + } + } +} + +internal class StringsCallback(private val continuation: Continuation>) : IStringsCallback.Stub() { + override fun onStrings(statusCode: Int, strings: MutableList) { + if (statusCode == Constants.STATUS_OK) { + continuation.resume(strings) + } else { + continuation.resumeWithException(RuntimeException("Status: $statusCode")) + } + } +} + +internal class StatusCallback(private val continuation: Continuation) : IStatusCallback.Stub() { + override fun onStatus(statusCode: Int) { + if (statusCode == Constants.STATUS_OK) { + continuation.resume(Unit) + } else { + continuation.resumeWithException(RuntimeException("Status: $statusCode")) + } + } +} diff --git a/client/src/main/kotlin/org/microg/nlp/client/GeocodeClient.kt b/client/src/main/kotlin/org/microg/nlp/client/GeocodeClient.kt new file mode 100644 index 0000000..149f6f4 --- /dev/null +++ b/client/src/main/kotlin/org/microg/nlp/client/GeocodeClient.kt @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.client + +import android.content.Context +import android.location.Address +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import org.microg.nlp.service.api.* +import org.microg.nlp.service.api.Constants.STATUS_OK +import java.util.concurrent.* +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class GeocodeClient(context: Context, lifecycle: Lifecycle) : BaseClient(context, lifecycle, { IGeocodeService.Stub.asInterface(it) }) { + private val syncThreads: ThreadPoolExecutor = ThreadPoolExecutor(1, Runtime.getRuntime().availableProcessors(), 1, TimeUnit.SECONDS, LinkedBlockingQueue()) + override val action: String + get() = Constants.ACTION_GEOCODE + + fun requestGeocodeSync(request: GeocodeRequest, options: Bundle = defaultOptions): List
= executeWithTimeout { + withConnectedServiceSync { service -> + service.requestGeocodeSync(request, options) + } + } + + suspend fun requestGeocode(request: GeocodeRequest, options: Bundle = defaultOptions): List
= withService { service -> + suspendCoroutine { + service.requestGeocode(request, AddressesCallback(it), options) + } + } + + fun requestReverseGeocodeSync(request: ReverseGeocodeRequest, options: Bundle = defaultOptions): List
= executeWithTimeout { + withConnectedServiceSync { service -> + service.requestReverseGeocodeSync(request, options) + } + } + + suspend fun requestReverseGeocode(request: ReverseGeocodeRequest, options: Bundle = defaultOptions): List
= withService { service -> + suspendCoroutine { + service.requestReverseGeocode(request, AddressesCallback(it), options) + } + } + + suspend fun getGeocodeBackends(options: Bundle = defaultOptions): List = withService { service -> + suspendCoroutine { + service.getGeocodeBackends(StringsCallback(it), options) + } + } + + suspend fun setGeocodeBackends(backends: List, options: Bundle = defaultOptions): Unit = withService { service -> + suspendCoroutine { + service.setGeocodeBackends(backends, StatusCallback(it), options) + } + } + + private fun executeWithTimeout(timeout: Long = CALL_TIMEOUT, action: () -> T): T { + var result: T? = null + val latch = CountDownLatch(1) + var err: Exception? = null + syncThreads.execute { + try { + result = 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() + } + + companion object { + private const val CALL_TIMEOUT = 10000L + } +} + +private class AddressesCallback(private val continuation: Continuation>) : IAddressesCallback.Stub() { + override fun onAddresses(statusCode: Int, addresses: List
?) { + if (statusCode == STATUS_OK) { + continuation.resume(addresses.orEmpty()) + } else { + continuation.resumeWithException(RuntimeException("Status: $statusCode")) + } + } +} diff --git a/client/src/main/kotlin/org/microg/nlp/client/IntentResolver.kt b/client/src/main/kotlin/org/microg/nlp/client/IntentResolver.kt new file mode 100644 index 0000000..97fa6a2 --- /dev/null +++ b/client/src/main/kotlin/org/microg/nlp/client/IntentResolver.kt @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.client + +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.ResolveInfo +import android.util.Log + +internal fun resolveIntent(context: Context, action: String): Intent? { + val intent = Intent(action) + + val pm = context.packageManager + var resolveInfos = pm.queryIntentServices(intent, 0) + if (resolveInfos.size > 1) { + + // Restrict to self if possible + val isSelf: (it: ResolveInfo) -> Boolean = { + it.serviceInfo.packageName == context.packageName + } + if (resolveInfos.size > 1 && resolveInfos.any(isSelf)) { + Log.d("IntentResolver", "Found more than one service for $action, restricted to own package " + context.packageName) + resolveInfos = resolveInfos.filter(isSelf) + } + + // Restrict to package with matching signature if possible + val isSelfSig: (it: ResolveInfo) -> Boolean = { + it.serviceInfo.packageName == context.packageName + } + if (resolveInfos.size > 1 && resolveInfos.any(isSelfSig)) { + Log.d("IntentResolver", "Found more than one service for $action, restricted to related packages") + resolveInfos = resolveInfos.filter(isSelfSig) + } + + // Restrict to system if any package is system + val isSystem: (it: ResolveInfo) -> Boolean = { + (it.serviceInfo?.applicationInfo?.flags ?: 0) and ApplicationInfo.FLAG_SYSTEM > 0 + } + if (resolveInfos.size > 1 && resolveInfos.any(isSystem)) { + Log.d("IntentResolver", "Found more than one service for $action, restricted to system packages") + resolveInfos = resolveInfos.filter(isSystem) + } + + val highestPriority: ResolveInfo? = resolveInfos.maxByOrNull { it.priority } + intent.setPackage(highestPriority!!.serviceInfo.packageName) + intent.setClassName(highestPriority.serviceInfo.packageName, highestPriority.serviceInfo.name) + if (resolveInfos.size > 1) { + Log.d("IntentResolver", "Found more than one service for $action, picked highest priority " + intent.component) + } + return intent + } else if (!resolveInfos.isEmpty()) { + intent.setPackage(resolveInfos[0].serviceInfo.packageName) + return intent + } else { + Log.w("IntentResolver", "No service to bind to, your system does not support unified service") + return null + } +} diff --git a/client/src/main/kotlin/org/microg/nlp/client/LocationClient.kt b/client/src/main/kotlin/org/microg/nlp/client/LocationClient.kt new file mode 100644 index 0000000..ed5a077 --- /dev/null +++ b/client/src/main/kotlin/org/microg/nlp/client/LocationClient.kt @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2020, 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.ServiceConnection +import android.location.Location +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.microg.nlp.service.api.* +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class LocationClient(context: Context, lifecycle: Lifecycle) : BaseClient(context, lifecycle, { ILocationService.Stub.asInterface(it) }) { + private val requests = hashSetOf() + private val requestsMutex = Mutex(false) + + override val action: String + get() = Constants.ACTION_LOCATION + + suspend fun getLastLocation(options: Bundle = defaultOptions): Location? = withService { service -> + suspendCoroutine { + service.getLastLocation(SingleLocationListener(it), options) + } + } + + suspend fun getLastLocationForBackend(componentName: ComponentName, options: Bundle = defaultOptions) = getLastLocationForBackend(componentName.packageName, componentName.className, null, options) + + suspend fun getLastLocationForBackend(packageName: String, className: String, signatureDigest: String? = null, options: Bundle = defaultOptions): Location? = withService { service -> + suspendCoroutine { + service.getLastLocationForBackend(packageName, className, signatureDigest, SingleLocationListener(it), options) + } + } + + private suspend fun withRequestService(v: suspend (ILocationService) -> T): T { + return requestsMutex.withLock { + try { + if (requests.isEmpty()) connect() + withService(v) + } finally { + if (requests.isEmpty()) disconnect() + } + } + } + + suspend fun updateLocationRequest(request: LocationRequest, options: Bundle = defaultOptions): Unit = withRequestService { service -> + suspendCoroutine { + service.updateLocationRequest(request, StatusCallback(it), options) + } + requests.removeAll { it.id == request.id } + requests.add(request) + } + + suspend fun cancelLocationRequestByListener(listener: ILocationListener, options: Bundle = defaultOptions): Unit = withRequestService { service -> + suspendCoroutine { + service.cancelLocationRequestByListener(listener, StatusCallback(it), options) + } + requests.removeAll { it.listener == listener } + } + + suspend fun cancelLocationRequestById(id: String, options: Bundle = defaultOptions): Unit = withRequestService { service -> + suspendCoroutine { + service.cancelLocationRequestById(id, StatusCallback(it), options) + } + requests.removeAll { it.id == id } + } + + suspend fun forceLocationUpdate(options: Bundle = defaultOptions): Unit = withService { service -> + suspendCoroutine { + service.forceLocationUpdate(StatusCallback(it), options) + } + } + + suspend fun getLocationBackends(options: Bundle = defaultOptions): List = withService { service -> + suspendCoroutine { + service.getLocationBackends(StringsCallback(it), options) + } + } + + suspend fun setLocationBackends(backends: List, options: Bundle = defaultOptions): Unit = withService { service -> + suspendCoroutine { + service.setLocationBackends(backends, StatusCallback(it), options) + } + } +} + +private class SingleLocationListener(private val continuation: Continuation) : ILocationListener.Stub() { + override fun onLocation(statusCode: Int, location: Location?) { + if (statusCode == Constants.STATUS_OK) { + continuation.resume(location) + } else { + continuation.resumeWithException(RuntimeException("Status: $statusCode")) + } + } +} diff --git a/client/src/main/kotlin/org/microg/nlp/client/UnifiedLocationClient.kt b/client/src/main/kotlin/org/microg/nlp/client/UnifiedLocationClient.kt index 141a705..e4c9340 100644 --- a/client/src/main/kotlin/org/microg/nlp/client/UnifiedLocationClient.kt +++ b/client/src/main/kotlin/org/microg/nlp/client/UnifiedLocationClient.kt @@ -9,8 +9,6 @@ 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 @@ -18,37 +16,31 @@ 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.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.lang.RuntimeException +import java.util.* import java.util.concurrent.* +import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.* -import kotlin.math.min private const val CALL_TIMEOUT = 10000L -class UnifiedLocationClient private constructor(context: Context) { +@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 context: WeakReference = WeakReference(context) private var service: UnifiedLocationService? = null private val syncThreads: ThreadPoolExecutor = ThreadPoolExecutor(1, Runtime.getRuntime().availableProcessors(), 1, TimeUnit.SECONDS, LinkedBlockingQueue()) private val waitingForService = arrayListOf>() private var timer: Timer? = null private var reconnectCount = 0 private val requests = CopyOnWriteArraySet() - private val coroutineScope = CoroutineScope(Dispatchers.IO + Job()) private val connection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { this@UnifiedLocationClient.onServiceConnected(name, service) @@ -82,56 +74,7 @@ class UnifiedLocationClient private constructor(context: Context) { 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) { - - // Restrict to self if possible - val isSelf: (it: ResolveInfo) -> Boolean = { - it.serviceInfo.packageName == context.packageName - } - if (resolveInfos.size > 1 && resolveInfos.any(isSelf)) { - Log.d(TAG, "Found more than one active unified service, restricted to own package " + context.packageName) - resolveInfos = resolveInfos.filter(isSelf) - } - - // Restrict to package with matching signature if possible - val isSelfSig: (it: ResolveInfo) -> Boolean = { - it.serviceInfo.packageName == context.packageName - } - if (resolveInfos.size > 1 && resolveInfos.any(isSelfSig)) { - Log.d(TAG, "Found more than one active unified service, restricted to related packages") - resolveInfos = resolveInfos.filter(isSelfSig) - } - - // Restrict to system if any package is system - val isSystem: (it: ResolveInfo) -> Boolean = { - (it.serviceInfo?.applicationInfo?.flags ?: 0) and ApplicationInfo.FLAG_SYSTEM > 0 - } - if (resolveInfos.size > 1 && resolveInfos.any(isSystem)) { - Log.d(TAG, "Found more than one active unified service, restricted to system packages") - resolveInfos = resolveInfos.filter(isSystem) - } - - val highestPriority: ResolveInfo? = resolveInfos.maxBy { it.priority } - intent.setPackage(highestPriority!!.serviceInfo.packageName) - intent.setClassName(highestPriority.serviceInfo.packageName, highestPriority.serviceInfo.name) - if (resolveInfos.size > 1) { - Log.d(TAG, "Found more than one active unified service, picked highest priority " + intent.component) - } - 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 - } - } + private fun resolve(): Intent? = resolveIntent(context, ACTION_UNIFIED_LOCATION_SERVICE) @Synchronized private fun updateBinding(): Boolean { @@ -167,7 +110,6 @@ class UnifiedLocationClient private constructor(context: Context) { @Synchronized private fun bind() { - val context = this.context.get() ?: return if (!bound) { Log.w(TAG, "Tried to bind while not being bound!") return @@ -186,7 +128,7 @@ class UnifiedLocationClient private constructor(context: Context) { @Synchronized private fun unbind() { try { - this.context.get()?.unbindService(connection) + this.context.unbindService(connection) } catch (ignored: Exception) { } @@ -207,29 +149,34 @@ class UnifiedLocationClient private constructor(context: Context) { 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)) - coroutineScope.launch { + lifecycleScope.launchWhenStarted { updateServiceInterval() updateBinding() } } + @Deprecated("Use LocationClient") fun removeLocationUpdates(listener: LocationListener) { - coroutineScope.launch { + lifecycleScope.launchWhenStarted { removeRequests(requests.filter { it.listener === listener }) } } @@ -310,6 +257,7 @@ class UnifiedLocationClient private constructor(context: Context) { return result ?: throw NullPointerException() } + @Deprecated("Use GeocoderClient") suspend fun getFromLocation(latitude: Double, longitude: Double, maxResults: Int, locale: String, timeout: Long = Long.MAX_VALUE): List
{ try { val service = refAndGetService() @@ -325,10 +273,12 @@ class UnifiedLocationClient private constructor(context: Context) { } } + @Deprecated("Use GeocoderClient") fun getFromLocationSync(latitude: Double, longitude: Double, maxResults: Int, locale: String, timeout: Long = CALL_TIMEOUT): List
= 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
{ return try { val service = refAndGetService() @@ -344,20 +294,12 @@ class UnifiedLocationClient private constructor(context: Context) { } } + @Deprecated("Use GeocoderClient") fun getFromLocationNameSync(locationName: String, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String, timeout: Long = CALL_TIMEOUT): List
= 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() - } - } - + @Deprecated("Use LocationClient") suspend fun getLocationBackends(): Array { try { return refAndGetService().locationBackends @@ -369,6 +311,7 @@ class UnifiedLocationClient private constructor(context: Context) { } } + @Deprecated("Use LocationClient") suspend fun setLocationBackends(backends: Array) { try { refAndGetService().locationBackends = backends @@ -379,6 +322,7 @@ class UnifiedLocationClient private constructor(context: Context) { } } + @Deprecated("Use GeocoderClient") suspend fun getGeocoderBackends(): Array { try { return refAndGetService().geocoderBackends @@ -390,6 +334,7 @@ class UnifiedLocationClient private constructor(context: Context) { } } + @Deprecated("Use GeocoderClient") suspend fun setGeocoderBackends(backends: Array) { try { refAndGetService().geocoderBackends = backends @@ -400,10 +345,12 @@ class UnifiedLocationClient private constructor(context: Context) { } } + @Deprecated("Use LocationClient") fun getLastLocationSync(timeout: Long = CALL_TIMEOUT): Location? = executeSyncWithTimeout(timeout) { getLastLocation() } + @Deprecated("Use LocationClient") suspend fun getLastLocation(): Location? { return try { refAndGetService().lastLocation @@ -415,6 +362,7 @@ class UnifiedLocationClient private constructor(context: Context) { } } + @Deprecated("Use LocationClient") suspend fun getLastLocationForBackend(packageName: String, className: String, signatureDigest: String? = null): Location? { return try { refAndGetService().getLastLocationForBackend(packageName, className, signatureDigest) @@ -439,30 +387,32 @@ class UnifiedLocationClient private constructor(context: Context) { } private suspend fun updateServiceInterval() { - var interval: Long = Long.MAX_VALUE + var minTime = Long.MAX_VALUE var requestSingle = false for (request in requests) { if (request.interval <= 0) { requestSingle = true + forceNextUpdate = true continue } - interval = min(interval, request.interval) + if (request.interval <= minTime) { + minTime = request.interval + } } - if (interval == Long.MAX_VALUE) { + if (minTime == Long.MAX_VALUE) { Log.d(TAG, "Disable automatic updates") - interval = 0 + minTime = 0 } else { - Log.d(TAG, "Set update interval to $interval") + Log.d(TAG, "Set update interval to $minTime") } - val service: UnifiedLocationService - try { - service = waitForService() + val service = try { + waitForService() } catch (e: Exception) { Log.w(TAG, e) return } try { - service.setUpdateInterval(interval, options) + service.setUpdateInterval(minTime, options) if (requestSingle) { Log.d(TAG, "Request single update (force update: $forceNextUpdate)") service.requestSingleUpdate(options) @@ -487,12 +437,12 @@ class UnifiedLocationClient private constructor(context: Context) { continuations.addAll(waitingForService) waitingForService.clear() } - coroutineScope.launch { + lifecycleScope.launchWhenStarted { try { Log.d(TAG, "Registering location callback") service.registerLocationCallback(object : LocationCallback.Stub() { override fun onLocationUpdate(location: Location) { - coroutineScope.launch { + lifecycleScope.launchWhenStarted { this@UnifiedLocationClient.onLocationUpdate(location) } } @@ -538,6 +488,8 @@ class UnifiedLocationClient private constructor(context: Context) { bindLater() } + override fun getLifecycle(): Lifecycle = lifecycle + interface LocationListener { fun onLocation(location: Location) @@ -592,20 +544,6 @@ class UnifiedLocationClient private constructor(context: Context) { 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" - 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 - } } } @@ -622,4 +560,4 @@ class AddressContinuation(private val continuation: Continuation>) // Ignore } } -} \ No newline at end of file +} diff --git a/compat/build.gradle b/compat/build.gradle index 2bd3d91..f228a9d 100644 --- a/compat/build.gradle +++ b/compat/build.gradle @@ -12,7 +12,15 @@ if (!sdkDir) { sdkDir = properties.getProperty('sdk.dir') } -sourceSets.main { - java.srcDirs = ['src/current/java', 'src/v9/java'] - compileClasspath += project.rootProject.files("$sdkDir/platforms/android-$androidCompileSdk/android.jar") +sourceSets { + main { + java { + srcDir 'src/current/java' + srcDir 'src/v9/java' + } + } +} + +dependencies { + compileOnly files("$sdkDir/platforms/android-$androidCompileSdk/android.jar") } diff --git a/geocode-v1/build.gradle b/geocode-v1/build.gradle index 9de8286..4c01f6c 100644 --- a/geocode-v1/build.gradle +++ b/geocode-v1/build.gradle @@ -4,6 +4,7 @@ */ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' apply plugin: 'maven-publish' apply plugin: 'signing' @@ -30,4 +31,9 @@ description = 'UnifiedNlp service to implement Geocode API v1' dependencies { implementation project(':client') compileOnly project(':compat') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" } diff --git a/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeProvider.java b/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeProvider.java deleted file mode 100644 index 9b4d984..0000000 --- a/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeProvider.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.nlp.geocode.v1; - -import android.content.Context; -import android.location.Address; -import android.location.GeocoderParams; -import android.util.Log; - -import org.microg.nlp.client.UnifiedLocationClient; - -import java.util.List; - -public class GeocodeProvider extends com.android.location.provider.GeocodeProvider { - private static final String TAG = "UGeocode"; - private Context context; - private static final long TIMEOUT = 10000; - - public GeocodeProvider(Context context) { - this.context = context; - UnifiedLocationClient.get(context).ref(); - } - - public void onDisable() { - UnifiedLocationClient.get(context).unref(); - } - - @Override - public String onGetFromLocation(double latitude, double longitude, int maxResults, GeocoderParams params, List
addrs) { - try { - return handleResult(addrs, UnifiedLocationClient.get(context).getFromLocationSync(latitude, longitude, maxResults, params.getLocale().toString(), TIMEOUT)); - } catch (Exception e) { - Log.w(TAG, e); - return e.getMessage(); - } - } - - @Override - public String onGetFromLocationName(String locationName, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, int maxResults, GeocoderParams params, List
addrs) { - try { - return handleResult(addrs, UnifiedLocationClient.get(context).getFromLocationNameSync(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, params.getLocale().toString(), TIMEOUT)); - } catch (Exception e) { - Log.w(TAG, e); - return e.getMessage(); - } - } - - private String handleResult(List
realResult, List
fuserResult) { - if (fuserResult.isEmpty()) { - return "no result"; - } else { - realResult.addAll(fuserResult); - return null; - } - } -} diff --git a/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeProvider.kt b/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeProvider.kt new file mode 100644 index 0000000..4e2d1cc --- /dev/null +++ b/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeProvider.kt @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.nlp.geocode.v1 + +import android.content.Context +import android.location.Address +import android.location.GeocoderParams +import android.os.Bundle +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.android.location.provider.GeocodeProvider +import org.microg.nlp.client.GeocodeClient +import org.microg.nlp.service.api.GeocodeRequest +import org.microg.nlp.service.api.LatLon +import org.microg.nlp.service.api.LatLonBounds +import org.microg.nlp.service.api.ReverseGeocodeRequest + +class GeocodeProvider(context: Context, lifecycle: Lifecycle) : GeocodeProvider() { + private val client: GeocodeClient = GeocodeClient(context, lifecycle) + + override fun onGetFromLocation(latitude: Double, longitude: Double, maxResults: Int, params: GeocoderParams, addrs: MutableList
): String? { + return try { + handleResult(addrs, client.requestReverseGeocodeSync( + ReverseGeocodeRequest(LatLon(latitude, longitude), maxResults, params.locale), + Bundle().apply { putString("packageName", params.clientPackage) } + )) + } catch (e: Exception) { + Log.w(TAG, e) + e.message + } + } + + override fun onGetFromLocationName(locationName: String?, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, maxResults: Int, params: GeocoderParams, addrs: MutableList
): String? { + return try { + handleResult(addrs, client.requestGeocodeSync( + GeocodeRequest(locationName!!, LatLonBounds(LatLon(lowerLeftLatitude, lowerLeftLongitude), LatLon(upperRightLatitude, upperRightLongitude)), maxResults, params.locale), + Bundle().apply { putString("packageName", params.clientPackage) } + )) + } catch (e: Exception) { + Log.w(TAG, e) + e.message + } + } + + private fun handleResult(realResult: MutableList
, fuserResult: List
): String? { + return if (fuserResult.isEmpty()) { + "no result" + } else { + realResult.addAll(fuserResult) + null + } + } + + suspend fun connect() = client.connect() + suspend fun disconnect() = client.disconnect() + + companion object { + private const val TAG = "GeocodeProvider" + private const val TIMEOUT: Long = 10000 + } +} diff --git a/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeService.java b/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeService.java deleted file mode 100644 index fb854da..0000000 --- a/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeService.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.nlp.geocode.v1; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; -import android.util.Log; - -public class GeocodeService extends Service { - private static final String TAG = "UnifiedGeocode"; - private GeocodeProvider provider; - - @Override - public synchronized IBinder onBind(Intent intent) { - Log.d(TAG, "onBind: "+intent); - if (provider == null) { - provider = new GeocodeProvider(this); - } - return provider.getBinder(); - } - - @Override - public synchronized boolean onUnbind(Intent intent) { - if (provider != null) { - provider.onDisable(); - } - return super.onUnbind(intent); - } -} diff --git a/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeService.kt b/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeService.kt new file mode 100644 index 0000000..378744c --- /dev/null +++ b/geocode-v1/src/main/java/org/microg/nlp/geocode/v1/GeocodeService.kt @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.nlp.geocode.v1 + +import android.content.Intent +import android.os.IBinder +import android.util.Log +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.runBlocking + +class GeocodeService : LifecycleService() { + private lateinit var provider: GeocodeProvider + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "Creating system service...") + provider = GeocodeProvider(this, lifecycle) + lifecycleScope.launchWhenStarted { provider.connect() } + Log.d(TAG, "Created system service.") + } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + Log.d(TAG, "onBind: $intent") + return provider.binder + } + + override fun onUnbind(intent: Intent): Boolean { + return super.onUnbind(intent) + } + + override fun onDestroy() { + runBlocking { provider.disconnect() } + super.onDestroy() + } + + companion object { + private const val TAG = "GeocodeService" + } +} diff --git a/location-v2/build.gradle b/location-v2/build.gradle index 68b7827..9e4f775 100644 --- a/location-v2/build.gradle +++ b/location-v2/build.gradle @@ -4,6 +4,7 @@ */ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' apply plugin: 'maven-publish' apply plugin: 'signing' @@ -30,4 +31,9 @@ description = 'UnifiedNlp service to implement Location API v2' dependencies { implementation project(':client') compileOnly project(':compat') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" } diff --git a/location-v2/src/main/java/org/microg/nlp/location/v2/LocationProvider.java b/location-v2/src/main/java/org/microg/nlp/location/v2/LocationProvider.java deleted file mode 100644 index 492b4dd..0000000 --- a/location-v2/src/main/java/org/microg/nlp/location/v2/LocationProvider.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.nlp.location.v2; - -import android.content.Context; -import android.location.Criteria; -import android.location.Location; -import android.os.Bundle; -import android.os.WorkSource; -import android.util.Log; - -import com.android.location.provider.LocationProviderBase; -import com.android.location.provider.ProviderPropertiesUnbundled; -import com.android.location.provider.ProviderRequestUnbundled; - -import org.microg.nlp.client.UnifiedLocationClient; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.List; - -import static android.location.LocationProvider.AVAILABLE; - -public class LocationProvider extends LocationProviderBase implements UnifiedLocationClient.LocationListener { - private static final List EXCLUDED_PACKAGES = Arrays.asList("android", "com.android.location.fused", "com.google.android.gms"); - private static final long FASTEST_REFRESH_INTERVAL = 30000; - private static final String TAG = "ULocation"; - private Context context; - - public LocationProvider(Context context) { - super(TAG, ProviderPropertiesUnbundled.create(false, false, false, false, true, true, true, Criteria.POWER_LOW, Criteria.ACCURACY_COARSE)); - this.context = context; - } - - @Override - public void onEnable() { - UnifiedLocationClient.get(context).ref(); - } - - @Override - public void onDisable() { - UnifiedLocationClient.get(context).unref(); - } - - @Override - public void onSetRequest(ProviderRequestUnbundled requests, WorkSource source) { - Log.v(TAG, "onSetRequest: " + requests + " by " + source); - String opPackageName = null; - try { - Field namesField = WorkSource.class.getDeclaredField("mNames"); - namesField.setAccessible(true); - String[] names = (String[]) namesField.get(source); - if (names != null) { - for (String name : names) { - if (!EXCLUDED_PACKAGES.contains(name)) { - opPackageName = name; - break; - } - } - } - } catch (Exception ignored) { - } - - long autoTime = Math.max(requests.getInterval(), FASTEST_REFRESH_INTERVAL); - boolean autoUpdate = requests.getReportLocation(); - - Log.v(TAG, "using autoUpdate=" + autoUpdate + " autoTime=" + autoTime); - - if (autoUpdate) { - UnifiedLocationClient.get(context).setOpPackageName(opPackageName); - UnifiedLocationClient.get(context).requestLocationUpdates(this, autoTime); - } else { - UnifiedLocationClient.get(context).removeLocationUpdates(this); - } - } - - public void unsetRequest() { - UnifiedLocationClient.get(context).removeLocationUpdates(this); - } - - @SuppressWarnings("deprecation") - @Override - public int onGetStatus(Bundle extras) { - return AVAILABLE; - } - - @Override - public long onGetStatusUpdateTime() { - return 0; - } - - @Override - public void onLocation(Location location) { - reportLocation(location); - } -} diff --git a/location-v2/src/main/java/org/microg/nlp/location/v2/LocationProvider.kt b/location-v2/src/main/java/org/microg/nlp/location/v2/LocationProvider.kt new file mode 100644 index 0000000..a424591 --- /dev/null +++ b/location-v2/src/main/java/org/microg/nlp/location/v2/LocationProvider.kt @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.nlp.location.v2 + +import android.content.Context +import android.location.Criteria +import android.location.Location +import android.location.LocationProvider +import android.os.Bundle +import android.os.SystemClock +import android.os.WorkSource +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.android.location.provider.LocationProviderBase +import com.android.location.provider.ProviderPropertiesUnbundled +import com.android.location.provider.ProviderRequestUnbundled +import kotlinx.coroutines.launch +import org.microg.nlp.client.LocationClient +import org.microg.nlp.service.api.Constants.STATUS_OK +import org.microg.nlp.service.api.ILocationListener +import org.microg.nlp.service.api.LocationRequest +import java.io.FileDescriptor +import java.io.PrintWriter +import java.util.* + +class LocationProvider(private val context: Context, private val lifecycle: Lifecycle) : LocationProviderBase(TAG, ProviderPropertiesUnbundled.create(false, false, false, false, true, true, true, Criteria.POWER_LOW, Criteria.ACCURACY_COARSE)), LifecycleOwner { + private val client: LocationClient = LocationClient(context, lifecycle) + private val id = UUID.randomUUID().toString() + private var statusUpdateTime = SystemClock.elapsedRealtime() + private val listener = object : ILocationListener.Stub() { + override fun onLocation(statusCode: Int, location: Location?) { + if (statusCode == STATUS_OK && location != null) { + val reportableLocation = Location(location) + for (key in reportableLocation.extras.keySet().toList()) { + if (key?.startsWith("org.microg.nlp.") == true) { + reportableLocation.extras.remove(key) + } + } + Log.d(TAG, "reportLocation: $reportableLocation") + reportLocation(reportableLocation) + } + } + } + private var opPackageName: String? = null + private var autoTime = Long.MAX_VALUE + private var autoUpdate = false + + init { + client.defaultOptions.putString("source", "LocationProvider") + client.defaultOptions.putString("requestId", id) + } + + override fun onEnable() { + Log.d(TAG, "onEnable") + statusUpdateTime = SystemClock.elapsedRealtime() + } + + override fun onDisable() { + Log.d(TAG, "onDisable") + unsetRequest() + statusUpdateTime = SystemClock.elapsedRealtime() + } + + override fun onSetRequest(requests: ProviderRequestUnbundled, source: WorkSource) { + Log.v(TAG, "onSetRequest: $requests by $source") + opPackageName = null + try { + val namesField = WorkSource::class.java.getDeclaredField("mNames") + namesField.isAccessible = true + val names = namesField[source] as Array + if (names != null) { + for (name in names) { + if (!EXCLUDED_PACKAGES.contains(name)) { + opPackageName = name + break + } + } + } + } catch (ignored: Exception) { + } + autoTime = requests.interval.coerceAtLeast(FASTEST_REFRESH_INTERVAL) + autoUpdate = requests.reportLocation + Log.v(TAG, "using autoUpdate=$autoUpdate autoTime=$autoTime") + lifecycleScope.launch { + updateRequest() + } + } + + suspend fun updateRequest() { + if (client.isConnected()) { + if (autoUpdate) { + client.packageName = opPackageName ?: context.packageName + client.updateLocationRequest(LocationRequest(listener, autoTime, Int.MAX_VALUE, id)) + } else { + client.cancelLocationRequestById(id) + } + } + } + + fun unsetRequest() { + lifecycleScope.launch { + client.cancelLocationRequestById(id) + } + } + + override fun onGetStatus(extras: Bundle?): Int { + return LocationProvider.AVAILABLE + } + + override fun onGetStatusUpdateTime(): Long { + return statusUpdateTime + } + + override fun onSendExtraCommand(command: String?, extras: Bundle?): Boolean { + Log.d(TAG, "onSendExtraCommand: $command, $extras") + return false + } + + override fun onDump(fd: FileDescriptor?, pw: PrintWriter?, args: Array?) { + dump(pw) + } + + fun dump(writer: PrintWriter?) { + writer?.println("ID: $id") + writer?.println("connected: ${client.isConnectedUnsafe}") + writer?.println("active: $autoUpdate") + writer?.println("interval: $autoTime") + } + + suspend fun connect() { + Log.d(TAG, "Connecting to userspace service...") + client.connect() + updateRequest() + Log.d(TAG, "Connected to userspace service.") + } + suspend fun disconnect() = client.disconnect() + + override fun getLifecycle(): Lifecycle = lifecycle + + companion object { + private val EXCLUDED_PACKAGES = listOf("android", "com.android.location.fused", "com.google.android.gms") + private const val FASTEST_REFRESH_INTERVAL: Long = 2500 + private const val TAG = "LocationProvider" + } +} diff --git a/location-v2/src/main/java/org/microg/nlp/location/v2/LocationService.java b/location-v2/src/main/java/org/microg/nlp/location/v2/LocationService.java deleted file mode 100644 index 87f3ff4..0000000 --- a/location-v2/src/main/java/org/microg/nlp/location/v2/LocationService.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.nlp.location.v2; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; -import android.util.Log; - -public class LocationService extends Service { - private static final String TAG = "ULocation"; - private LocationProvider provider; - - @Override - public synchronized IBinder onBind(Intent intent) { - Log.d(TAG, "onBind: "+intent); - if (provider == null) { - provider = new LocationProvider(this); - } - return provider.getBinder(); - } - - @Override - public synchronized boolean onUnbind(Intent intent) { - if (provider != null) { - provider.unsetRequest(); - } - return super.onUnbind(intent); - } -} diff --git a/location-v2/src/main/java/org/microg/nlp/location/v2/LocationService.kt b/location-v2/src/main/java/org/microg/nlp/location/v2/LocationService.kt new file mode 100644 index 0000000..6e67f60 --- /dev/null +++ b/location-v2/src/main/java/org/microg/nlp/location/v2/LocationService.kt @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.nlp.location.v2 + +import androidx.lifecycle.LifecycleService +import android.content.Intent +import android.os.IBinder +import android.util.Log +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.runBlocking +import java.io.FileDescriptor +import java.io.PrintWriter + +open class LocationService : LifecycleService() { + private lateinit var provider: LocationProvider + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "Creating system service...") + provider = LocationProvider(this, lifecycle) + lifecycleScope.launchWhenStarted { provider.connect() } + Log.d(TAG, "Created system service.") + } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + Log.d(TAG, "onBind: $intent") + return provider.binder + } + + override fun onDestroy() { + runBlocking { provider.disconnect() } + super.onDestroy() + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array?) { + if (!this::provider.isInitialized) { + writer?.println("Not yet initialized") + } + provider.dump(writer) + } + + companion object { + private const val TAG = "LocationService" + } +} diff --git a/location-v3/build.gradle b/location-v3/build.gradle index abcf30d..fb4e3fc 100644 --- a/location-v3/build.gradle +++ b/location-v3/build.gradle @@ -4,6 +4,7 @@ */ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' apply plugin: 'maven-publish' apply plugin: 'signing' @@ -29,4 +30,9 @@ description = 'UnifiedNlp service to implement Location API v3' dependencies { implementation project(':location-v2') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" } diff --git a/location-v3/src/main/java/org/microg/nlp/location/v3/LocationService.java b/location-v3/src/main/java/org/microg/nlp/location/v3/LocationService.java deleted file mode 100644 index 028e99b..0000000 --- a/location-v3/src/main/java/org/microg/nlp/location/v3/LocationService.java +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.nlp.location.v3; - -public class LocationService extends org.microg.nlp.location.v2.LocationService { -} diff --git a/location-v3/src/main/java/org/microg/nlp/location/v3/LocationService.kt b/location-v3/src/main/java/org/microg/nlp/location/v3/LocationService.kt new file mode 100644 index 0000000..79ca18f --- /dev/null +++ b/location-v3/src/main/java/org/microg/nlp/location/v3/LocationService.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.nlp.location.v3 + +import org.microg.nlp.location.v2.LocationService + +class LocationService : LocationService() diff --git a/ui/build.gradle b/ui/build.gradle index d759d9d..8b3f0bd 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -14,8 +14,9 @@ apply plugin: 'signing' android { compileSdkVersion androidCompileSdk buildToolsVersion "$androidBuildVersionTools" - dataBinding { - enabled = true + + buildFeatures { + dataBinding = true } defaultConfig { diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/BackendConfiguration.kt b/ui/src/main/kotlin/org/microg/nlp/ui/BackendConfiguration.kt index fb4382f..1c18ef4 100644 --- a/ui/src/main/kotlin/org/microg/nlp/ui/BackendConfiguration.kt +++ b/ui/src/main/kotlin/org/microg/nlp/ui/BackendConfiguration.kt @@ -23,7 +23,8 @@ import org.microg.nlp.api.Constants.ACTION_GEOCODER_BACKEND import org.microg.nlp.api.Constants.ACTION_LOCATION_BACKEND import org.microg.nlp.api.GeocoderBackend import org.microg.nlp.api.LocationBackend -import org.microg.nlp.client.UnifiedLocationClient +import org.microg.nlp.client.GeocodeClient +import org.microg.nlp.client.LocationClient import org.microg.nlp.ui.model.BackendInfo import org.microg.nlp.ui.model.BackendType import java.security.MessageDigest @@ -31,6 +32,7 @@ import java.security.NoSuchAlgorithmException private fun Array.without(entry: BackendInfo): Array = filterNot { it == entry.unsignedComponent || it.startsWith("${entry.unsignedComponent}/") }.toTypedArray() +private fun List.without(entry: BackendInfo): List = filterNot { it == entry.unsignedComponent || it.startsWith("${entry.unsignedComponent}/") } suspend fun BackendInfo.updateEnabled(fragment: Fragment, newValue: Boolean) { Log.d("USettings", "updateEnabled $signedComponent = $newValue") @@ -107,20 +109,31 @@ private suspend fun BackendInfo.enable(fragment: Fragment): Boolean { return false } } - val client = UnifiedLocationClient[activity] - when (type) { - BackendType.LOCATION -> client.setLocationBackends(client.getLocationBackends().without(this) + signedComponent) - BackendType.GEOCODER -> client.setGeocoderBackends(client.getGeocoderBackends().without(this) + signedComponent) + when(type) { + BackendType.LOCATION -> { + val client = LocationClient(activity, activity.lifecycle) + client.setLocationBackends(client.getLocationBackends().without(this) + signedComponent) + } + BackendType.GEOCODER -> { + val client = GeocodeClient(activity, activity.lifecycle) + client.setGeocodeBackends(client.getGeocodeBackends().without(this) + signedComponent) + } } Log.w("USettings", "Enabled backend $signedComponent") return true } private suspend fun BackendInfo.disable(fragment: Fragment): Boolean { - val client = UnifiedLocationClient[fragment.requireContext()] - when (type) { - BackendType.LOCATION -> client.setLocationBackends(client.getLocationBackends().without(this)) - BackendType.GEOCODER -> client.setGeocoderBackends(client.getGeocoderBackends().without(this)) + val activity = fragment.requireActivity() as AppCompatActivity + when(type) { + BackendType.LOCATION -> { + val client = LocationClient(activity, activity.lifecycle) + client.setLocationBackends(client.getLocationBackends().without(this)) + } + BackendType.GEOCODER -> { + val client = GeocodeClient(activity, activity.lifecycle) + client.setGeocodeBackends(client.getGeocodeBackends().without(this)) + } } return true } diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt b/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt index 7872514..3c84976 100644 --- a/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt +++ b/ui/src/main/kotlin/org/microg/nlp/ui/BackendDetailsFragment.kt @@ -26,14 +26,16 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.microg.nlp.client.UnifiedLocationClient -import org.microg.nlp.ui.model.BackendType.GEOCODER -import org.microg.nlp.ui.model.BackendType.LOCATION +import org.microg.nlp.client.GeocodeClient +import org.microg.nlp.client.LocationClient +import org.microg.nlp.service.api.LatLon +import org.microg.nlp.service.api.ReverseGeocodeRequest import org.microg.nlp.ui.databinding.BackendDetailsBinding import org.microg.nlp.ui.model.BackendDetailsCallback import org.microg.nlp.ui.model.BackendInfo import org.microg.nlp.ui.model.BackendType +import org.microg.nlp.ui.model.BackendType.GEOCODER +import org.microg.nlp.ui.model.BackendType.LOCATION import java.util.* class BackendDetailsFragment : Fragment(R.layout.backend_details), BackendDetailsCallback { @@ -80,6 +82,8 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details), BackendDetail } private lateinit var binding: BackendDetailsBinding + private lateinit var locationClient: LocationClient + private lateinit var geocodeClient: GeocodeClient override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { binding = BackendDetailsBinding.inflate(inflater, container, false) @@ -90,13 +94,13 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details), BackendDetail override fun onResume() { super.onResume() binding.switchWidget.trackTintList = switchBarTrackTintColor - UnifiedLocationClient[requireContext()].ref() + locationClient = LocationClient(requireContext(), lifecycle) + geocodeClient = GeocodeClient(requireContext(), lifecycle) lifecycleScope.launchWhenStarted { initContent(createBackendInfo()) } } override fun onPause() { super.onPause() - UnifiedLocationClient[requireContext()].unref() } private suspend fun initContent(entry: BackendInfo?) { @@ -115,20 +119,18 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details), BackendDetail if (entry.type == LOCATION && entry.enabled.get()) { if (updateInProgress) return updateInProgress = true - val client = UnifiedLocationClient[requireContext()] - val locationTemp = client.getLastLocationForBackend(entry.serviceInfo.packageName, entry.serviceInfo.name, entry.firstSignatureDigest) + val locationTemp = locationClient.getLastLocationForBackend(entry.serviceInfo.packageName, entry.serviceInfo.name, entry.firstSignatureDigest) val location = when (locationTemp) { null -> { delay(500L) // Wait short time to ensure backend was activated Log.d(TAG, "Location was not available, requesting once") - client.forceNextUpdate = true - client.getSingleLocation() - val secondAttempt = client.getLastLocationForBackend(entry.serviceInfo.packageName, entry.serviceInfo.name, entry.firstSignatureDigest) + locationClient.forceLocationUpdate() + val secondAttempt = locationClient.getLastLocationForBackend(entry.serviceInfo.packageName, entry.serviceInfo.name, entry.firstSignatureDigest) if (secondAttempt == null) { Log.d(TAG, "Location still not available, waiting or giving up") delay(WAIT_FOR_RESULT) - client.getLastLocationForBackend(entry.serviceInfo.packageName, entry.serviceInfo.name, entry.firstSignatureDigest) + locationClient.getLastLocationForBackend(entry.serviceInfo.packageName, entry.serviceInfo.name, entry.firstSignatureDigest) } else { secondAttempt } @@ -137,7 +139,7 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details), BackendDetail } ?: return var locationString = "${location.latitude.toStringWithDigits(6)}, ${location.longitude.toStringWithDigits(6)}" - val address = client.getFromLocation(location.latitude, location.longitude, 1, Locale.getDefault().toString()).singleOrNull() + val address = geocodeClient.requestReverseGeocode(ReverseGeocodeRequest(LatLon(location.latitude, location.longitude))).singleOrNull() if (address != null) { val addressLine = StringBuilder() var i = 0 @@ -175,8 +177,8 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details), BackendDetail val serviceInfo = context?.packageManager?.getServiceInfo(ComponentName(packageName, name), GET_META_DATA) ?: return null val enabledBackends = when (type) { - GEOCODER -> UnifiedLocationClient[requireContext()].getGeocoderBackends() - LOCATION -> UnifiedLocationClient[requireContext()].getLocationBackends() + LOCATION -> locationClient.getLocationBackends() + GEOCODER -> geocodeClient.getGeocodeBackends() } val info = BackendInfo(serviceInfo, type, firstSignatureDigest(requireContext(), packageName)) info.enabled.set(enabledBackends.contains(info.signedComponent) || enabledBackends.contains(info.unsignedComponent)) @@ -186,9 +188,8 @@ class BackendDetailsFragment : Fragment(R.layout.backend_details), BackendDetail override fun onEnabledChange(entry: BackendInfo?, newValue: Boolean) { if (entry?.enabled?.get() == newValue) return Log.d(TAG, "onEnabledChange: ${entry?.signedComponent} = $newValue") - val activity = requireActivity() as AppCompatActivity entry?.enabled?.set(newValue) - activity.lifecycleScope.launch { + lifecycleScope.launchWhenStarted { entry?.updateEnabled(this@BackendDetailsFragment, newValue) initContent(entry) } diff --git a/ui/src/main/kotlin/org/microg/nlp/ui/BackendListFragment.kt b/ui/src/main/kotlin/org/microg/nlp/ui/BackendListFragment.kt index d8dd47b..d603db2 100644 --- a/ui/src/main/kotlin/org/microg/nlp/ui/BackendListFragment.kt +++ b/ui/src/main/kotlin/org/microg/nlp/ui/BackendListFragment.kt @@ -23,10 +23,10 @@ import androidx.navigation.NavController import androidx.navigation.fragment.findNavController import androidx.navigation.navOptions import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.launch import org.microg.nlp.api.Constants.ACTION_GEOCODER_BACKEND import org.microg.nlp.api.Constants.ACTION_LOCATION_BACKEND -import org.microg.nlp.client.UnifiedLocationClient +import org.microg.nlp.client.GeocodeClient +import org.microg.nlp.client.LocationClient import org.microg.nlp.ui.databinding.BackendListBinding import org.microg.nlp.ui.databinding.BackendListEntryBinding import org.microg.nlp.ui.model.BackendInfo @@ -45,13 +45,11 @@ class BackendListFragment : Fragment(R.layout.backend_list), BackendListEntryCal override fun onResume() { super.onResume() - UnifiedLocationClient[requireContext()].ref() lifecycleScope.launchWhenStarted { updateAdapters() } } override fun onPause() { super.onPause() - UnifiedLocationClient[requireContext()].unref() } override fun onOpenDetails(entry: BackendInfo?) { @@ -65,25 +63,25 @@ class BackendListFragment : Fragment(R.layout.backend_list), BackendListEntryCal override fun onEnabledChange(entry: BackendInfo?, newValue: Boolean) { val activity = requireActivity() as AppCompatActivity - activity.lifecycleScope.launch { + lifecycleScope.launchWhenStarted { entry?.updateEnabled(this@BackendListFragment, newValue) } } private suspend fun updateAdapters() { val activity = requireActivity() as AppCompatActivity - locationAdapter.setEntries(createBackendInfoList(activity, Intent(ACTION_LOCATION_BACKEND), UnifiedLocationClient[activity].getLocationBackends(), BackendType.LOCATION)) - geocoderAdapter.setEntries(createBackendInfoList(activity, Intent(ACTION_GEOCODER_BACKEND), UnifiedLocationClient[activity].getGeocoderBackends(), BackendType.GEOCODER)) + locationAdapter.setEntries(createBackendInfoList(activity, Intent(ACTION_LOCATION_BACKEND), LocationClient(activity, lifecycle).getLocationBackends(), BackendType.LOCATION)) + geocoderAdapter.setEntries(createBackendInfoList(activity, Intent(ACTION_GEOCODER_BACKEND), GeocodeClient(activity, lifecycle).getGeocodeBackends(), BackendType.GEOCODER)) } - private fun createBackendInfoList(activity: AppCompatActivity, intent: Intent, enabledBackends: Array, type: BackendType): Array { + private fun createBackendInfoList(activity: AppCompatActivity, intent: Intent, enabledBackends: List, type: BackendType): Array { val backends = activity.packageManager.queryIntentServices(intent, GET_META_DATA).map { val info = BackendInfo(it.serviceInfo, type, firstSignatureDigest(activity, it.serviceInfo.packageName)) if (enabledBackends.contains(info.signedComponent) || enabledBackends.contains(info.unsignedComponent)) { info.enabled.set(true) } info.fillDetails(activity) - activity.lifecycleScope.launch { + lifecycleScope.launchWhenStarted { info.loadIntents(activity) } info