24 changed files with 863 additions and 380 deletions
@ -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<I>(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<ContinuedServiceConnection> { 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 <T> 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 <T> 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<ContinuedServiceConnection>) : 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<List<String>>) : IStringsCallback.Stub() { |
||||
override fun onStrings(statusCode: Int, strings: MutableList<String>) { |
||||
if (statusCode == Constants.STATUS_OK) { |
||||
continuation.resume(strings) |
||||
} else { |
||||
continuation.resumeWithException(RuntimeException("Status: $statusCode")) |
||||
} |
||||
} |
||||
} |
||||
|
||||
internal class StatusCallback(private val continuation: Continuation<Unit>) : IStatusCallback.Stub() { |
||||
override fun onStatus(statusCode: Int) { |
||||
if (statusCode == Constants.STATUS_OK) { |
||||
continuation.resume(Unit) |
||||
} else { |
||||
continuation.resumeWithException(RuntimeException("Status: $statusCode")) |
||||
} |
||||
} |
||||
} |
@ -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<IGeocodeService>(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<Address> = executeWithTimeout { |
||||
withConnectedServiceSync { service -> |
||||
service.requestGeocodeSync(request, options) |
||||
} |
||||
} |
||||
|
||||
suspend fun requestGeocode(request: GeocodeRequest, options: Bundle = defaultOptions): List<Address> = withService { service -> |
||||
suspendCoroutine { |
||||
service.requestGeocode(request, AddressesCallback(it), options) |
||||
} |
||||
} |
||||
|
||||
fun requestReverseGeocodeSync(request: ReverseGeocodeRequest, options: Bundle = defaultOptions): List<Address> = executeWithTimeout { |
||||
withConnectedServiceSync { service -> |
||||
service.requestReverseGeocodeSync(request, options) |
||||
} |
||||
} |
||||
|
||||
suspend fun requestReverseGeocode(request: ReverseGeocodeRequest, options: Bundle = defaultOptions): List<Address> = withService { service -> |
||||
suspendCoroutine { |
||||
service.requestReverseGeocode(request, AddressesCallback(it), options) |
||||
} |
||||
} |
||||
|
||||
suspend fun getGeocodeBackends(options: Bundle = defaultOptions): List<String> = withService { service -> |
||||
suspendCoroutine { |
||||
service.getGeocodeBackends(StringsCallback(it), options) |
||||
} |
||||
} |
||||
|
||||
suspend fun setGeocodeBackends(backends: List<String>, options: Bundle = defaultOptions): Unit = withService { service -> |
||||
suspendCoroutine { |
||||
service.setGeocodeBackends(backends, StatusCallback(it), options) |
||||
} |
||||
} |
||||
|
||||
private fun <T> 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<List<Address>>) : IAddressesCallback.Stub() { |
||||
override fun onAddresses(statusCode: Int, addresses: List<Address>?) { |
||||
if (statusCode == STATUS_OK) { |
||||
continuation.resume(addresses.orEmpty()) |
||||
} else { |
||||
continuation.resumeWithException(RuntimeException("Status: $statusCode")) |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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<ILocationService>(context, lifecycle, { ILocationService.Stub.asInterface(it) }) { |
||||
private val requests = hashSetOf<LocationRequest>() |
||||
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 <T> 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<Unit> { |
||||
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<Unit> { |
||||
service.cancelLocationRequestByListener(listener, StatusCallback(it), options) |
||||
} |
||||
requests.removeAll { it.listener == listener } |
||||
} |
||||
|
||||
suspend fun cancelLocationRequestById(id: String, options: Bundle = defaultOptions): Unit = withRequestService { service -> |
||||
suspendCoroutine<Unit> { |
||||
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<String> = withService { service -> |
||||
suspendCoroutine { |
||||
service.getLocationBackends(StringsCallback(it), options) |
||||
} |
||||
} |
||||
|
||||
suspend fun setLocationBackends(backends: List<String>, options: Bundle = defaultOptions): Unit = withService { service -> |
||||
suspendCoroutine { |
||||
service.setLocationBackends(backends, StatusCallback(it), options) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private class SingleLocationListener(private val continuation: Continuation<Location?>) : ILocationListener.Stub() { |
||||
override fun onLocation(statusCode: Int, location: Location?) { |
||||
if (statusCode == Constants.STATUS_OK) { |
||||
continuation.resume(location) |
||||
} else { |
||||
continuation.resumeWithException(RuntimeException("Status: $statusCode")) |
||||
} |
||||
} |
||||
} |
@ -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<Address> 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<Address> 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<Address> realResult, List<Address> fuserResult) { |
||||
if (fuserResult.isEmpty()) { |
||||
return "no result"; |
||||
} else { |
||||
realResult.addAll(fuserResult); |
||||
return null; |
||||
} |
||||
} |
||||
} |
@ -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<Address>): 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<Address>): 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<Address>, fuserResult: List<Address>): 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 |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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" |
||||
} |
||||
} |
@ -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<String> 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); |
||||
} |
||||
} |
@ -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<String> |
||||
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<out String>?) { |
||||
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" |
||||
} |
||||
} |