From aff82ab98c2eda43b1fb80ffa4b102bea968a9b2 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Fri, 24 Jan 2020 23:06:33 +0100 Subject: [PATCH] Add service --- service/build.gradle | 73 +++++++ service/src/main/AndroidManifest.xml | 45 +++++ .../nlp/service/AbstractBackendHelper.kt | 124 ++++++++++++ .../nlp/service/GeocodeBackendHelper.kt | 86 ++++++++ .../org/microg/nlp/service/GeocodeFuser.kt | 73 +++++++ .../nlp/service/LocationBackendHelper.kt | 135 +++++++++++++ .../org/microg/nlp/service/LocationFuser.kt | 147 ++++++++++++++ .../nlp/service/PackageChangedReceiver.kt | 48 +++++ .../org/microg/nlp/service/Preferences.kt | 38 ++++ .../UnifiedLocationServiceEntryPoint.kt | 48 +++++ .../service/UnifiedLocationServiceInstance.kt | 75 +++++++ .../nlp/service/UnifiedLocationServiceRoot.kt | 187 ++++++++++++++++++ 12 files changed, 1079 insertions(+) create mode 100644 service/build.gradle create mode 100644 service/src/main/AndroidManifest.xml create mode 100644 service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/GeocodeBackendHelper.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/GeocodeFuser.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/LocationBackendHelper.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/LocationFuser.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/PackageChangedReceiver.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/Preferences.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceEntryPoint.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceInstance.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceRoot.kt diff --git a/service/build.gradle b/service/build.gradle new file mode 100644 index 0000000..e606b6a --- /dev/null +++ b/service/build.gradle @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'maven-publish' + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +dependencies { + implementation project(':api') + implementation project(':client') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" +} + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + pom { + name = 'UnifiedNlp Service' + description = 'UnifiedNlp service library' + url = 'https://github.com/microg/UnifiedNlp' + licenses { + license { + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'microg' + name = 'microG Team' + } + developer { + id = 'mar-v-in' + name = 'Marvin W.' + } + } + scm { + url = 'https://github.com/microg/UnifiedNlp' + connection = 'scm:git:https://github.com/microg/UnifiedNlp.git' + developerConnection = 'scm:git:ssh://github.com/microg/UnifiedNlp.git' + } + } + + from components.release + } + } + } +} diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7de6e10 --- /dev/null +++ b/service/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt b/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt new file mode 100644 index 0000000..21611fc --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.annotation.SuppressLint +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.Signature +import android.os.IBinder +import android.os.RemoteException +import android.util.Log + +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +fun Array?.isNotNullOrEmpty(): Boolean { + return this != null && this.isNotEmpty() +} + +abstract class AbstractBackendHelper(private val TAG: String, private val context: Context, val serviceIntent: Intent, val signatureDigest: String?) : ServiceConnection { + private var bound: Boolean = false + + protected abstract fun close() + + protected abstract fun hasBackend(): Boolean + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + bound = true + Log.d(TAG, "Bound to: $name") + } + + override fun onServiceDisconnected(name: ComponentName) { + bound = false + Log.d(TAG, "Unbound from: $name") + } + + fun unbind() { + if (bound) { + if (hasBackend()) { + try { + close() + } catch (e: Exception) { + Log.w(TAG, e) + } + + } + try { + Log.d(TAG, "Unbinding from: $serviceIntent") + context.unbindService(this) + } catch (e: Exception) { + Log.w(TAG, e) + } + + bound = false + } + } + + fun bind() { + if (!bound) { + Log.d(TAG, "Binding to: $serviceIntent sig: $signatureDigest") + if (signatureDigest == null) { + Log.w(TAG, "No signature digest provided. Aborting.") + return + } + if (serviceIntent.getPackage() == null) { + Log.w(TAG, "Intent is not properly resolved, can't verify signature. Aborting.") + return + } + if (signatureDigest != firstSignatureDigest(context, serviceIntent.getPackage())) { + Log.w(TAG, "Target signature does not match selected package (" + signatureDigest + " = " + firstSignatureDigest(context, serviceIntent.getPackage()) + "). Aborting.") + return + } + try { + context.bindService(serviceIntent, this, Context.BIND_AUTO_CREATE) + } catch (e: Exception) { + Log.w(TAG, e) + } + + } + } + + companion object { + @Suppress("DEPRECATION") + @SuppressLint("PackageManagerGetSignatures") + fun firstSignatureDigest(context: Context, packageName: String?): String? { + val packageManager = context.packageManager + val info: PackageInfo? + try { + info = packageManager.getPackageInfo(packageName!!, PackageManager.GET_SIGNATURES) + } catch (e: PackageManager.NameNotFoundException) { + return null + } + + if (info?.signatures.isNotNullOrEmpty()) { + for (sig in info.signatures) { + sha256sum(sig.toByteArray())?.let { return it } + } + } + return null + } + + private fun sha256sum(bytes: ByteArray): String? { + try { + val md = MessageDigest.getInstance("SHA-256") + val digest = md.digest(bytes) + val sb = StringBuilder(2 * digest.size) + for (b in digest) { + sb.append(String.format("%02x", b)) + } + return sb.toString() + } catch (e: NoSuchAlgorithmException) { + return null + } + } + } + +} diff --git a/service/src/main/kotlin/org/microg/nlp/service/GeocodeBackendHelper.kt b/service/src/main/kotlin/org/microg/nlp/service/GeocodeBackendHelper.kt new file mode 100644 index 0000000..c9003aa --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/GeocodeBackendHelper.kt @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.location.Address +import android.os.IBinder +import android.os.RemoteException +import android.util.Log +import org.microg.nlp.api.GeocoderBackend + +class GeocodeBackendHelper(context: Context, serviceIntent: Intent, signatureDigest: String?) : AbstractBackendHelper(TAG, context, serviceIntent, signatureDigest) { + private var backend: GeocoderBackend? = null + + fun getFromLocation(latitude: Double, longitude: Double, maxResults: Int, + locale: String): List
{ + if (backend == null) { + Log.d(TAG, "Not (yet) bound.") + return emptyList() + } + try { + return backend!!.getFromLocation(latitude, longitude, maxResults, locale) ?: emptyList() + } catch (e: Exception) { + Log.w(TAG, e) + unbind() + return emptyList() + } + + } + + fun getFromLocationName(locationName: String, maxResults: Int, + lowerLeftLatitude: Double, lowerLeftLongitude: Double, + upperRightLatitude: Double, upperRightLongitude: Double, + locale: String): List
{ + if (backend == null) { + Log.d(TAG, "Not (yet) bound.") + return emptyList() + } + try { + return backend!!.getFromLocationName(locationName, maxResults, lowerLeftLatitude, + lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale) ?: emptyList() + } catch (e: Exception) { + Log.w(TAG, e) + unbind() + return emptyList() + } + + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + super.onServiceConnected(name, service) + backend = GeocoderBackend.Stub.asInterface(service) + if (backend != null) { + try { + backend!!.open() + } catch (e: Exception) { + Log.w(TAG, e) + unbind() + } + + } + } + + override fun onServiceDisconnected(name: ComponentName) { + super.onServiceDisconnected(name) + backend = null + } + + @Throws(RemoteException::class) + public override fun close() { + backend!!.close() + } + + public override fun hasBackend(): Boolean { + return backend != null + } + + companion object { + private val TAG = "UnifiedGeocoder" + } +} \ No newline at end of file diff --git a/service/src/main/kotlin/org/microg/nlp/service/GeocodeFuser.kt b/service/src/main/kotlin/org/microg/nlp/service/GeocodeFuser.kt new file mode 100644 index 0000000..6f34592 --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/GeocodeFuser.kt @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.content.Context +import android.content.Intent +import android.location.Address +import org.microg.nlp.api.Constants.ACTION_GEOCODER_BACKEND +import java.util.ArrayList + +class GeocodeFuser(private val context: Context) { + private val backendHelpers = ArrayList() + + init { + reset() + } + + fun reset() { + unbind() + backendHelpers.clear() + for (backend in Preferences(context).geocoderBackends) { + val parts = backend.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (parts.size >= 2) { + val intent = Intent(ACTION_GEOCODER_BACKEND) + intent.setPackage(parts[0]) + intent.setClassName(parts[0], parts[1]) + backendHelpers.add(GeocodeBackendHelper(context, intent, if (parts.size >= 3) parts[2] else null)) + } + } + } + + fun bind() { + for (backendHelper in backendHelpers) { + backendHelper.bind() + } + } + + fun unbind() { + for (backendHelper in backendHelpers) { + backendHelper.unbind() + } + } + + fun destroy() { + unbind() + backendHelpers.clear() + } + + fun getFromLocation(latitude: Double, longitude: Double, maxResults: Int, locale: String): List
? { + if (backendHelpers.isEmpty()) + return null + val result = ArrayList
() + for (backendHelper in backendHelpers) { + val backendResult = backendHelper.getFromLocation(latitude, longitude, maxResults, locale) + result.addAll(backendResult) + } + return result + } + + fun getFromLocationName(locationName: String, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String): List
? { + if (backendHelpers.isEmpty()) + return null + val result = ArrayList
() + for (backendHelper in backendHelpers) { + val backendResult = backendHelper.getFromLocationName(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale) + result.addAll(backendResult) + } + return result + } +} diff --git a/service/src/main/kotlin/org/microg/nlp/service/LocationBackendHelper.kt b/service/src/main/kotlin/org/microg/nlp/service/LocationBackendHelper.kt new file mode 100644 index 0000000..0bfe238 --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/LocationBackendHelper.kt @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.annotation.TargetApi +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.location.Location +import android.net.wifi.WifiManager +import android.os.* +import android.util.Log +import org.microg.nlp.api.Constants.LOCATION_EXTRA_BACKEND_COMPONENT +import org.microg.nlp.api.Constants.LOCATION_EXTRA_BACKEND_PROVIDER +import org.microg.nlp.api.LocationBackend +import org.microg.nlp.api.LocationCallback + +class LocationBackendHelper(context: Context, private val locationFuser: LocationFuser, serviceIntent: Intent, signatureDigest: String?) : AbstractBackendHelper(TAG, context, serviceIntent, signatureDigest) { + private val callback = Callback() + private var backend: LocationBackend? = null + private var updateWaiting: Boolean = false + var lastLocation: Location? = null + private set(location) { + if (location == null || !location.hasAccuracy()) { + return + } + if (location.extras == null) { + location.extras = Bundle() + } + location.extras.putString(LOCATION_EXTRA_BACKEND_PROVIDER, location.provider) + location.extras.putString(LOCATION_EXTRA_BACKEND_COMPONENT, + serviceIntent.component!!.flattenToShortString()) + location.provider = "network" + if (!location.hasAccuracy()) { + location.accuracy = 50000f + } + if (location.time <= 0) { + location.time = System.currentTimeMillis() + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + updateElapsedRealtimeNanos(location) + } + field = location + } + + /** + * Requests a location update from the backend. + * + * @return The location reported by the backend. This may be null if a backend cannot determine its + * location, or if it is going to return a location asynchronously. + */ + fun update(): Location? { + var result: Location? = null + if (backend == null) { + Log.d(TAG, "Not (yet) bound.") + updateWaiting = true + } else { + updateWaiting = false + try { + result = backend?.update() + if (result == null) { + Log.d(TAG, "Received no location from ${serviceIntent.component!!.flattenToShortString()}") + } else { + Log.d(TAG, "Received location from ${serviceIntent.component!!.flattenToShortString()} with time ${result.time} (last was ${lastLocation?.time ?: 0})") + if (this.lastLocation == null || result.time > this.lastLocation!!.time) { + lastLocation = result + locationFuser.reportLocation() + } + } + } catch (e: Exception) { + Log.w(TAG, e) + unbind() + } + + } + return result + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private fun updateElapsedRealtimeNanos(location: Location) { + if (location.elapsedRealtimeNanos <= 0) { + location.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() + } + } + + @Throws(RemoteException::class) + public override fun close() { + Log.d(TAG, "Calling close") + backend!!.close() + } + + public override fun hasBackend(): Boolean { + return backend != null + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + super.onServiceConnected(name, service) + backend = LocationBackend.Stub.asInterface(service) + if (backend != null) { + try { + Log.d(TAG, "Calling open") + backend!!.open(callback) + if (updateWaiting) { + update() + } + } catch (e: Exception) { + Log.w(TAG, e) + unbind() + } + + } + } + + override fun onServiceDisconnected(name: ComponentName) { + super.onServiceDisconnected(name) + backend = null + } + + private inner class Callback : LocationCallback.Stub() { + override fun report(location: Location?) { + val lastLocation = lastLocation + if (location == null || lastLocation != null && location.time > 0 && location.time <= lastLocation.getTime()) + return + this@LocationBackendHelper.lastLocation = location + locationFuser.reportLocation() + } + } + + companion object { + private val TAG = "UnifiedLocation" + } +} diff --git a/service/src/main/kotlin/org/microg/nlp/service/LocationFuser.kt b/service/src/main/kotlin/org/microg/nlp/service/LocationFuser.kt new file mode 100644 index 0000000..2ee7896 --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/LocationFuser.kt @@ -0,0 +1,147 @@ +/* + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.content.Context +import android.content.Intent +import android.location.Location +import android.location.LocationManager +import android.util.Log + +import java.util.ArrayList +import java.util.Collections +import java.util.Comparator + +import org.microg.nlp.api.Constants.ACTION_LOCATION_BACKEND +import org.microg.nlp.api.Constants.LOCATION_EXTRA_OTHER_BACKENDS + +class LocationFuser(private val context: Context, private val root: UnifiedLocationServiceRoot) { + + private val backendHelpers = ArrayList() + private var fusing = false + private var lastLocationReportTime: Long = 0 + + init { + reset() + } + + fun reset() { + unbind() + backendHelpers.clear() + lastLocationReportTime = 0 + for (backend in Preferences(context).locationBackends) { + Log.d(TAG, "Backend: $backend") + val parts = backend.split("/".toRegex()).dropLastWhile(String::isEmpty).toTypedArray() + if (parts.size >= 2) { + val intent = Intent(ACTION_LOCATION_BACKEND) + intent.setPackage(parts[0]) + intent.setClassName(parts[0], parts[1]) + backendHelpers.add(LocationBackendHelper(context, this, intent, if (parts.size >= 3) parts[2] else null)) + } + } + } + + fun unbind() { + for (handler in backendHelpers) { + handler.unbind() + } + } + + fun bind() { + fusing = false + for (handler in backendHelpers) { + handler.bind() + } + } + + fun destroy() { + unbind() + backendHelpers.clear() + } + + fun update() { + var hasUpdates = false + fusing = true + for (handler in backendHelpers) { + if (handler.update() != null) + hasUpdates = true + } + fusing = false + if (hasUpdates) + updateLocation() + } + + fun updateLocation() { + val locations = ArrayList() + for (handler in backendHelpers) { + handler.lastLocation?.let { locations.add(it) } + } + val location = mergeLocations(locations) + if (location != null) { + location.provider = LocationManager.NETWORK_PROVIDER + if (lastLocationReportTime < location.time) { + lastLocationReportTime = location.time + Log.v(TAG, "Fused location: $location") + root.reportLocation(location) + } else { + Log.v(TAG, "Ignoring location update as it's older than other provider.") + } + } + } + + private fun mergeLocations(locations: List): Location? { + Collections.sort(locations, LocationComparator.INSTANCE) + if (locations.isEmpty()) return null + if (locations.size == 1) return locations[0] + val location = Location(locations[0]) + val backendResults = ArrayList() + for (backendResult in locations) { + if (locations[0] == backendResult) continue + backendResults.add(backendResult) + } + if (!backendResults.isEmpty()) { + location.extras.putParcelableArrayList(LOCATION_EXTRA_OTHER_BACKENDS, backendResults) + } + return location + } + + fun reportLocation() { + if (fusing) + return + updateLocation() + } + + fun getLastLocationForBackend(packageName: String, className: String, signatureDigest: String?): Location? = + backendHelpers.find { + it.serviceIntent.`package` == packageName && it.serviceIntent.component?.className == className && (signatureDigest == null || it.signatureDigest == null || it.signatureDigest == signatureDigest) + }?.lastLocation + + class LocationComparator : Comparator { + + /** + * @return whether {@param lhs} is better than {@param rhs} + */ + override fun compare(lhs: Location?, rhs: Location?): Int { + if (lhs === rhs) return 0 + if (lhs == null) return 1 + if (rhs == null) return -1 + if (!lhs.hasAccuracy()) return 1 + if (!rhs.hasAccuracy()) return -1 + if (rhs.time > lhs.time + SWITCH_ON_FRESHNESS_CLIFF_MS) return 1 + if (lhs.time > rhs.time + SWITCH_ON_FRESHNESS_CLIFF_MS) return -1 + return (lhs.accuracy - rhs.accuracy).toInt() + } + + companion object { + val INSTANCE = LocationComparator() + val SWITCH_ON_FRESHNESS_CLIFF_MS: Long = 30000 // 30 seconds + } + } + + companion object { + private val TAG = "UnifiedLocation" + } +} diff --git a/service/src/main/kotlin/org/microg/nlp/service/PackageChangedReceiver.kt b/service/src/main/kotlin/org/microg/nlp/service/PackageChangedReceiver.kt new file mode 100644 index 0000000..ca8c2ad --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/PackageChangedReceiver.kt @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.Intent.* +import android.util.Log + +import org.microg.nlp.client.UnifiedLocationClient + +class PackageChangedReceiver : BroadcastReceiver() { + + private fun isProtectedAction(action: String) = when (action) { + ACTION_PACKAGE_CHANGED, ACTION_PACKAGE_REMOVED, ACTION_PACKAGE_REPLACED, ACTION_PACKAGE_RESTARTED -> true + else -> false + } + + override fun onReceive(context: Context, intent: Intent) { + Log.d(TAG, "Intent received: $intent") + if (intent.action?.let { isProtectedAction(it) } != true) return + + val packageName = intent.data!!.schemeSpecificPart + val preferences = Preferences(context) + for (backend in preferences.locationBackends) { + if (backend.startsWith("$packageName/")) { + Log.d(TAG, "Reloading location service for $packageName") + suspend { UnifiedLocationClient[context].reloadPreferences() } + return + } + } + for (backend in preferences.geocoderBackends) { + if (backend.startsWith("$packageName/")) { + Log.d(TAG, "Reloading geocoding service for $packageName") + suspend { UnifiedLocationClient[context].reloadPreferences() } + return + } + } + } + + companion object { + private const val TAG = "UnifiedService" + } +} diff --git a/service/src/main/kotlin/org/microg/nlp/service/Preferences.kt b/service/src/main/kotlin/org/microg/nlp/service/Preferences.kt new file mode 100644 index 0000000..3e45dcc --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/Preferences.kt @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.content.Context +import android.content.SharedPreferences + +class Preferences(private val context: Context) { + + private val sharedPreferences: SharedPreferences + get() = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + + var locationBackends: Array + get() = splitBackendString(sharedPreferences.getString(PREF_LOCATION_BACKENDS, null)) + set(backends) { + sharedPreferences.edit().putString(PREF_LOCATION_BACKENDS, backends.joinToString("|")).apply() + } + + var geocoderBackends: Array + get() = splitBackendString(sharedPreferences.getString(PREF_GEOCODER_BACKENDS, null)) + set(backends) { + sharedPreferences.edit().putString(PREF_GEOCODER_BACKENDS, backends.joinToString("|")).apply() + } + + companion object { + private val PREFERENCES_NAME = "unified_nlp" + private val PREF_LOCATION_BACKENDS = "location_backends" + private val PREF_GEOCODER_BACKENDS = "geocoder_backends" + + private fun splitBackendString(backendString: String?): Array { + return backendString?.split("\\|".toRegex())?.dropLastWhile(String::isEmpty)?.toTypedArray() + ?: emptyArray() + } + } +} diff --git a/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceEntryPoint.kt b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceEntryPoint.kt new file mode 100644 index 0000000..b4d43c5 --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceEntryPoint.kt @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.util.Log + +class UnifiedLocationServiceEntryPoint : Service() { + private var root: UnifiedLocationServiceRoot? = null + + @Synchronized + fun destroy() { + if (root != null) { + root!!.destroy() + root = null + } + } + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "onCreate") + destroy() + } + + override fun onBind(intent: Intent): IBinder? { + Log.d(TAG, "onBind: $intent") + synchronized(this) { + if (root == null) { + root = UnifiedLocationServiceRoot(this) + } + return root!!.asBinder() + } + } + + override fun onDestroy() { + Log.d(TAG, "onDestroy") + destroy() + } + + companion object { + private val TAG = "ULocService" + } +} diff --git a/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceInstance.kt b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceInstance.kt new file mode 100644 index 0000000..b4f31db --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceInstance.kt @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.location.Location +import android.os.Bundle +import android.os.RemoteException +import android.util.Log + +import android.os.Binder.getCallingUid +import org.microg.nlp.client.UnifiedLocationClient + +class UnifiedLocationServiceInstance(private val root: UnifiedLocationServiceRoot) : UnifiedLocationService.Default() { + private var callback: LocationCallback? = null + private var interval: Long = 0 + private var singleUpdatePending = false + private val callingPackage = root.context.packageManager.getNameForUid(getCallingUid()) + + fun reportLocation(location: Location) { + try { + if (callback != null) { + callback!!.onLocationUpdate(location) + } + if (singleUpdatePending) { + singleUpdatePending = false + root.updateLocationInterval() + } + } catch (e: RemoteException) { + root.onDisconnected(this) + } + + } + + fun getInterval(): Long { + // TODO: Do not report interval if client should no longer receive + return if (singleUpdatePending) UnifiedLocationServiceRoot.MIN_LOCATION_INTERVAL else interval + } + + @Throws(RemoteException::class) + override fun registerLocationCallback(callback: LocationCallback, options: Bundle) { + Log.d(TAG, "registerLocationCallback[$callingPackage]") + this.callback = callback + } + + override fun setUpdateInterval(interval: Long, options: Bundle) { + Log.d(TAG, "setUpdateInterval[$callingPackage] interval: $interval") + this.interval = interval + root.updateLocationInterval() + } + + override fun requestSingleUpdate(options: Bundle) { + val lastLocation = root.lastReportedLocation + if (lastLocation == null || lastLocation.time < System.currentTimeMillis() - UnifiedLocationServiceRoot.MAX_LOCATION_AGE || options.getBoolean(UnifiedLocationClient.KEY_FORCE_NEXT_UPDATE, false)) { + Log.d(TAG, "requestSingleUpdate[$callingPackage] requesting new location") + singleUpdatePending = true + root.locationFuser.update() + root.updateLocationInterval() + } else { + Log.d(TAG, "requestSingleUpdate[$callingPackage] using last location ") + try { + this.callback!!.onLocationUpdate(lastLocation) + } catch (e: RemoteException) { + root.onDisconnected(this) + throw e + } + } + } + + companion object { + private val TAG = "ULocService" + } +} diff --git a/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceRoot.kt b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceRoot.kt new file mode 100644 index 0000000..f4e99be --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceRoot.kt @@ -0,0 +1,187 @@ +/* + * SPDX-FileCopyrightText: 2019, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.Manifest +import android.content.Context +import android.location.Location +import android.os.Binder +import android.os.Bundle +import android.os.Process.myUid +import android.util.Log +import java.util.* +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import kotlin.collections.ArrayList +import kotlin.collections.set +import kotlin.math.max +import kotlin.math.min + +class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntryPoint) : UnifiedLocationService.Stub() { + private val instances = HashMap() + private val geocoderThreads: ThreadPoolExecutor = ThreadPoolExecutor(1, Runtime.getRuntime().availableProcessors(), 1, TimeUnit.SECONDS, LinkedBlockingQueue()) + private val timer: Timer = Timer("location-requests") + private var timerTask: TimerTask? = null + private var lastTime: Long = 0 + val locationFuser: LocationFuser = LocationFuser(service, this) + val geocodeFuser: GeocodeFuser = GeocodeFuser(service) + var lastReportedLocation: Location? = null + private set + private var interval: Long = 0 + + val context: Context + get() = service + + init { + try { + locationFuser.bind() + geocodeFuser.bind() + } catch (e: Exception) { + Log.w(TAG, "Failed loading preferences", e) + } + } + + val instance: UnifiedLocationServiceInstance + @Synchronized get() { + checkLocationPermission() + val instance = instances[Binder.getCallingPid()] ?: UnifiedLocationServiceInstance(this) + instances[Binder.getCallingPid()] = instance + return instance + } + + @Synchronized + fun reportLocation(location: Location) { + for (instance in ArrayList(instances.values)) { + instance.reportLocation(location) + } + lastReportedLocation = location + } + + @Synchronized + fun onDisconnected(instance: UnifiedLocationServiceInstance) { + var instancePid: Int? = null + for (pid in instances.keys) { + if (instances[pid] === instance) instancePid = pid + } + if (instancePid != null) { + instances.remove(instancePid) + } + } + + fun destroy() { + locationFuser.destroy() + geocodeFuser.destroy() + } + + @Synchronized + fun updateLocationInterval() { + var interval: Long = Long.MAX_VALUE + for (instance in ArrayList(instances.values)) { + val implInterval = instance.getInterval() + if (implInterval <= 0) continue + interval = min(interval, implInterval) + } + interval = max(interval, MIN_LOCATION_INTERVAL) + + if (this.interval == interval) return + this.interval = interval + + timerTask?.cancel() + timerTask = null + + if (interval < Long.MAX_VALUE) { + Log.d(TAG, "Set merged location interval to $interval") + val timerTask = object : TimerTask() { + override fun run() { + lastTime = System.currentTimeMillis() + locationFuser.update() + } + } + timer.scheduleAtFixedRate(timerTask, min(interval, max(0, interval - (System.currentTimeMillis() - lastTime))), interval) + this.timerTask = timerTask + } else { + Log.d(TAG, "Disable location updates") + } + } + + private fun checkLocationPermission() { + service.enforceCallingPermission(Manifest.permission.ACCESS_COARSE_LOCATION, "coarse location permission required") + } + + override fun registerLocationCallback(callback: LocationCallback, options: Bundle) { + instance.registerLocationCallback(callback, options) + } + + override fun setUpdateInterval(interval: Long, options: Bundle) { + instance.setUpdateInterval(interval, options) + } + + override fun requestSingleUpdate(options: Bundle) { + instance.requestSingleUpdate(options) + } + + override fun getFromLocationWithOptions(latitude: Double, longitude: Double, maxResults: Int, locale: String, options: Bundle, callback: AddressCallback) { + geocoderThreads.execute { + callback.onResult(geocodeFuser.getFromLocation(latitude, longitude, maxResults, locale)) + } + } + + override fun getFromLocationNameWithOptions(locationName: String, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String, options: Bundle, callback: AddressCallback) { + geocoderThreads.execute { + callback.onResult(geocodeFuser.getFromLocationName(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale)) + } + } + + override fun getLocationBackends(): Array { + return Preferences(service).locationBackends + } + + override fun setLocationBackends(backends: Array) { + if (Binder.getCallingUid() != myUid()) throw SecurityException("Only allowed from same UID") + Preferences(service).locationBackends = backends + reloadPreferences() + } + + override fun getGeocoderBackends(): Array { + return Preferences(service).geocoderBackends + } + + override fun setGeocoderBackends(backends: Array) { + if (Binder.getCallingUid() != myUid()) throw SecurityException("Only allowed from same UID") + Preferences(service).locationBackends = backends + reloadPreferences() + } + + override fun reloadPreferences() { + if (Binder.getCallingUid() != myUid()) throw SecurityException("Only allowed from same UID") + reset() + } + + override fun getLastLocation(): Location? { + checkLocationPermission() + return lastReportedLocation + } + + override fun getLastLocationForBackend(packageName: String, className: String, signatureDigest: String?): Location? { + checkLocationPermission() + return locationFuser.getLastLocationForBackend(packageName, className, signatureDigest) + } + + @Synchronized + fun reset() { + locationFuser.reset() + locationFuser.bind() + geocodeFuser.reset() + geocodeFuser.bind() + } + + companion object { + private val TAG = "ULocService" + val MIN_LOCATION_INTERVAL = 2500L + val MAX_LOCATION_AGE = 3600000L + } +}