From d0921825761346a551556234bd27f03345c18bbb Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 18 Jan 2022 13:44:56 +0100 Subject: [PATCH] Add new split service logic --- .../java/org/microg/nlp/api/Constants.java | 3 + build.gradle | 20 +- service-api/build.gradle | 32 ++ service-api/src/main/AndroidManifest.xml | 7 + .../nlp/service/api/GeocodeRequest.aidl | 8 + .../nlp/service/api/IAddressesCallback.aidl | 12 + .../nlp/service/api/IGeocodeService.aidl | 24 ++ .../nlp/service/api/ILocationListener.aidl | 12 + .../nlp/service/api/ILocationService.aidl | 26 ++ .../nlp/service/api/IStatusCallback.aidl | 10 + .../nlp/service/api/IStringsCallback.aidl | 10 + .../nlp/service/api/LocationRequest.aidl | 8 + .../service/api/ReverseGeocodeRequest.aidl | 8 + .../org/microg/nlp/service/api/Constants.java | 20 + .../nlp/service/api/GeocodeRequest.java | 45 +++ .../org/microg/nlp/service/api/LatLon.java | 50 +++ .../microg/nlp/service/api/LatLonBounds.java | 22 ++ .../nlp/service/api/LocationRequest.java | 41 +++ .../service/api/ReverseGeocodeRequest.java | 42 +++ service/build.gradle | 3 +- service/src/main/AndroidManifest.xml | 18 +- .../nlp/service/AbstractBackendHelper.kt | 11 +- .../nlp/service/AsyncGeocoderBackend.kt | 43 ++- .../nlp/service/GeocodeBackendHelper.kt | 84 ----- .../org/microg/nlp/service/GeocodeFuser.kt | 142 ++++++- .../org/microg/nlp/service/GeocodeService.kt | 196 ++++++++++ .../nlp/service/LocationBackendHelper.kt | 134 ------- .../org/microg/nlp/service/LocationFuser.kt | 147 +++++++- .../org/microg/nlp/service/LocationService.kt | 348 ++++++++++++++++++ .../nlp/service/PackageChangedReceiver.kt | 6 +- .../UnifiedLocationServiceEntryPoint.kt | 47 +-- .../service/UnifiedLocationServiceInstance.kt | 13 +- .../nlp/service/UnifiedLocationServiceRoot.kt | 44 ++- settings.gradle | 1 + 34 files changed, 1343 insertions(+), 294 deletions(-) create mode 100644 service-api/build.gradle create mode 100644 service-api/src/main/AndroidManifest.xml create mode 100644 service-api/src/main/aidl/org/microg/nlp/service/api/GeocodeRequest.aidl create mode 100644 service-api/src/main/aidl/org/microg/nlp/service/api/IAddressesCallback.aidl create mode 100644 service-api/src/main/aidl/org/microg/nlp/service/api/IGeocodeService.aidl create mode 100644 service-api/src/main/aidl/org/microg/nlp/service/api/ILocationListener.aidl create mode 100644 service-api/src/main/aidl/org/microg/nlp/service/api/ILocationService.aidl create mode 100644 service-api/src/main/aidl/org/microg/nlp/service/api/IStatusCallback.aidl create mode 100644 service-api/src/main/aidl/org/microg/nlp/service/api/IStringsCallback.aidl create mode 100644 service-api/src/main/aidl/org/microg/nlp/service/api/LocationRequest.aidl create mode 100644 service-api/src/main/aidl/org/microg/nlp/service/api/ReverseGeocodeRequest.aidl create mode 100644 service-api/src/main/java/org/microg/nlp/service/api/Constants.java create mode 100644 service-api/src/main/java/org/microg/nlp/service/api/GeocodeRequest.java create mode 100644 service-api/src/main/java/org/microg/nlp/service/api/LatLon.java create mode 100644 service-api/src/main/java/org/microg/nlp/service/api/LatLonBounds.java create mode 100644 service-api/src/main/java/org/microg/nlp/service/api/LocationRequest.java create mode 100644 service-api/src/main/java/org/microg/nlp/service/api/ReverseGeocodeRequest.java delete mode 100644 service/src/main/kotlin/org/microg/nlp/service/GeocodeBackendHelper.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/GeocodeService.kt delete mode 100644 service/src/main/kotlin/org/microg/nlp/service/LocationBackendHelper.kt create mode 100644 service/src/main/kotlin/org/microg/nlp/service/LocationService.kt diff --git a/api/src/main/java/org/microg/nlp/api/Constants.java b/api/src/main/java/org/microg/nlp/api/Constants.java index 1c352d8..21d8e45 100644 --- a/api/src/main/java/org/microg/nlp/api/Constants.java +++ b/api/src/main/java/org/microg/nlp/api/Constants.java @@ -12,8 +12,11 @@ public class Constants { public static final String ACTION_FORCE_LOCATION = "org.microg.nlp.FORCE_LOCATION"; public static final String PERMISSION_FORCE_LOCATION = "org.microg.permission.FORCE_COARSE_LOCATION"; public static final String INTENT_EXTRA_LOCATION = "location"; + @Deprecated public static final String LOCATION_EXTRA_BACKEND_PROVIDER = "SERVICE_BACKEND_PROVIDER"; + @Deprecated public static final String LOCATION_EXTRA_BACKEND_COMPONENT = "SERVICE_BACKEND_COMPONENT"; + @Deprecated public static final String LOCATION_EXTRA_OTHER_BACKENDS = "OTHER_BACKEND_RESULTS"; public static final String METADATA_BACKEND_SETTINGS_ACTIVITY = "org.microg.nlp.BACKEND_SETTINGS_ACTIVITY"; public static final String METADATA_BACKEND_ABOUT_ACTIVITY = "org.microg.nlp.BACKEND_ABOUT_ACTIVITY"; diff --git a/build.gradle b/build.gradle index ce495fb..f16d931 100644 --- a/build.gradle +++ b/build.gradle @@ -4,23 +4,23 @@ */ buildscript { - ext.kotlinVersion = '1.3.72' - ext.coroutineVersion = '1.3.7' + ext.kotlinVersion = '1.6.10' + ext.coroutineVersion = '1.5.2' - ext.appcompatVersion = '1.1.0' - ext.fragmentVersion = '1.2.5' - ext.lifecycleVersion = '2.2.0' - ext.navigationVersion = '2.3.0' + ext.appcompatVersion = '1.4.0' + ext.fragmentVersion = '1.4.0' + ext.lifecycleVersion = '2.4.0' + ext.navigationVersion = '2.3.5' ext.preferenceVersion = '1.1.1' - ext.recyclerviewVersion = '1.1.0' + ext.recyclerviewVersion = '1.2.0' - ext.androidBuildGradleVersion = '3.6.3' + ext.androidBuildGradleVersion = '7.0.4' - ext.androidBuildVersionTools = '29.0.3' + ext.androidBuildVersionTools = '30.0.2' ext.androidMinSdk = 9 ext.androidTargetSdk = 29 - ext.androidCompileSdk = 29 + ext.androidCompileSdk = 31 repositories { jcenter() diff --git a/service-api/build.gradle b/service-api/build.gradle new file mode 100644 index 0000000..c0cf2c0 --- /dev/null +++ b/service-api/build.gradle @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2022, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +dependencies { + api "org.microg:safe-parcel:1.7.0" +} + +apply from: '../gradle/publish.gradle' + +description = 'API interfaces and helpers to access UnifiedNlp service' diff --git a/service-api/src/main/AndroidManifest.xml b/service-api/src/main/AndroidManifest.xml new file mode 100644 index 0000000..11dbdf5 --- /dev/null +++ b/service-api/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/service-api/src/main/aidl/org/microg/nlp/service/api/GeocodeRequest.aidl b/service-api/src/main/aidl/org/microg/nlp/service/api/GeocodeRequest.aidl new file mode 100644 index 0000000..3dd7272 --- /dev/null +++ b/service-api/src/main/aidl/org/microg/nlp/service/api/GeocodeRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2022, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +parcelable GeocodeRequest; diff --git a/service-api/src/main/aidl/org/microg/nlp/service/api/IAddressesCallback.aidl b/service-api/src/main/aidl/org/microg/nlp/service/api/IAddressesCallback.aidl new file mode 100644 index 0000000..6973b4f --- /dev/null +++ b/service-api/src/main/aidl/org/microg/nlp/service/api/IAddressesCallback.aidl @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2022, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +import android.location.Address; + +interface IAddressesCallback { + oneway void onAddresses(int statusCode, in List
location) ; +} diff --git a/service-api/src/main/aidl/org/microg/nlp/service/api/IGeocodeService.aidl b/service-api/src/main/aidl/org/microg/nlp/service/api/IGeocodeService.aidl new file mode 100644 index 0000000..825bf4a --- /dev/null +++ b/service-api/src/main/aidl/org/microg/nlp/service/api/IGeocodeService.aidl @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2022, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +import android.location.Address; +import org.microg.nlp.service.api.GeocodeRequest; +import org.microg.nlp.service.api.IAddressesCallback; +import org.microg.nlp.service.api.IStatusCallback; +import org.microg.nlp.service.api.IStringsCallback; +import org.microg.nlp.service.api.ReverseGeocodeRequest; + +interface IGeocodeService { + oneway void requestGeocode(in GeocodeRequest request, IAddressesCallback callback, in Bundle options) = 0; + oneway void requestReverseGeocode(in ReverseGeocodeRequest request, IAddressesCallback callback, in Bundle options) = 1; + List
requestGeocodeSync(in GeocodeRequest request, in Bundle options) = 2; + List
requestReverseGeocodeSync(in ReverseGeocodeRequest request, in Bundle options) = 3; + + oneway void reloadPreferences(IStatusCallback callback, in Bundle options) = 20; + oneway void getGeocodeBackends(IStringsCallback callback, in Bundle options) = 21; + oneway void setGeocodeBackends(in List backends, IStatusCallback callback, in Bundle options) = 22; +} diff --git a/service-api/src/main/aidl/org/microg/nlp/service/api/ILocationListener.aidl b/service-api/src/main/aidl/org/microg/nlp/service/api/ILocationListener.aidl new file mode 100644 index 0000000..cf28ff9 --- /dev/null +++ b/service-api/src/main/aidl/org/microg/nlp/service/api/ILocationListener.aidl @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2022, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +import android.location.Location; + +interface ILocationListener { + oneway void onLocation(int statusCode, in Location location); +} diff --git a/service-api/src/main/aidl/org/microg/nlp/service/api/ILocationService.aidl b/service-api/src/main/aidl/org/microg/nlp/service/api/ILocationService.aidl new file mode 100644 index 0000000..31a2f50 --- /dev/null +++ b/service-api/src/main/aidl/org/microg/nlp/service/api/ILocationService.aidl @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2022, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +import android.location.Location; +import org.microg.nlp.service.api.ILocationListener; +import org.microg.nlp.service.api.IStatusCallback; +import org.microg.nlp.service.api.IStringsCallback; +import org.microg.nlp.service.api.LocationRequest; + +interface ILocationService { + oneway void getLastLocation(ILocationListener listener, in Bundle options) = 0; + oneway void getLastLocationForBackend(String packageName, String className, String signatureDigest, ILocationListener listener, in Bundle options) = 1; + + oneway void updateLocationRequest(in LocationRequest request, IStatusCallback callback, in Bundle options) = 10; + oneway void cancelLocationRequestByListener(ILocationListener listener, IStatusCallback callback, in Bundle options) = 11; + oneway void cancelLocationRequestById(String id, IStatusCallback callback, in Bundle options) = 12; + oneway void forceLocationUpdate(IStatusCallback callback, in Bundle options) = 13; + + oneway void reloadPreferences(IStatusCallback callback, in Bundle options) = 20; + oneway void getLocationBackends(IStringsCallback callback, in Bundle options) = 21; + oneway void setLocationBackends(in List backends, IStatusCallback callback, in Bundle options) = 22; +} diff --git a/service-api/src/main/aidl/org/microg/nlp/service/api/IStatusCallback.aidl b/service-api/src/main/aidl/org/microg/nlp/service/api/IStatusCallback.aidl new file mode 100644 index 0000000..9f45bfb --- /dev/null +++ b/service-api/src/main/aidl/org/microg/nlp/service/api/IStatusCallback.aidl @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2022, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +interface IStatusCallback { + oneway void onStatus(int statusCode); +} diff --git a/service-api/src/main/aidl/org/microg/nlp/service/api/IStringsCallback.aidl b/service-api/src/main/aidl/org/microg/nlp/service/api/IStringsCallback.aidl new file mode 100644 index 0000000..3f34b50 --- /dev/null +++ b/service-api/src/main/aidl/org/microg/nlp/service/api/IStringsCallback.aidl @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2022, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +interface IStringsCallback { + oneway void onStrings(int statusCode, in List strings); +} diff --git a/service-api/src/main/aidl/org/microg/nlp/service/api/LocationRequest.aidl b/service-api/src/main/aidl/org/microg/nlp/service/api/LocationRequest.aidl new file mode 100644 index 0000000..cb77e43 --- /dev/null +++ b/service-api/src/main/aidl/org/microg/nlp/service/api/LocationRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2022, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +parcelable LocationRequest; diff --git a/service-api/src/main/aidl/org/microg/nlp/service/api/ReverseGeocodeRequest.aidl b/service-api/src/main/aidl/org/microg/nlp/service/api/ReverseGeocodeRequest.aidl new file mode 100644 index 0000000..e05bb88 --- /dev/null +++ b/service-api/src/main/aidl/org/microg/nlp/service/api/ReverseGeocodeRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2022, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +parcelable ReverseGeocodeRequest; diff --git a/service-api/src/main/java/org/microg/nlp/service/api/Constants.java b/service-api/src/main/java/org/microg/nlp/service/api/Constants.java new file mode 100644 index 0000000..6bf0b0f --- /dev/null +++ b/service-api/src/main/java/org/microg/nlp/service/api/Constants.java @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +public final class Constants { + public static final int STATUS_OK = 0; + public static final int STATUS_NOT_IMPLEMENTED = 1; + public static final int STATUS_PERMISSION_ERROR = 2; + public static final int STATUS_INVALID_ARGS = 3; + + public static final String ACTION_LOCATION = "org.microg.nlp.service.LOCATION"; + public static final String ACTION_GEOCODE = "org.microg.nlp.service.GEOCODE"; + + public static final String LOCATION_EXTRA_BACKEND_PROVIDER = "org.microg.nlp.extra.SERVICE_BACKEND_PROVIDER"; + public static final String LOCATION_EXTRA_BACKEND_COMPONENT = "org.microg.nlp.extra.SERVICE_BACKEND_COMPONENT"; + public static final String LOCATION_EXTRA_OTHER_BACKENDS = "org.microg.nlp.extra.OTHER_BACKEND_RESULTS"; +} diff --git a/service-api/src/main/java/org/microg/nlp/service/api/GeocodeRequest.java b/service-api/src/main/java/org/microg/nlp/service/api/GeocodeRequest.java new file mode 100644 index 0000000..a35733e --- /dev/null +++ b/service-api/src/main/java/org/microg/nlp/service/api/GeocodeRequest.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +import org.microg.safeparcel.AutoSafeParcelable; + +import java.util.Locale; + +public class GeocodeRequest extends AutoSafeParcelable { + @Field(1) + public String locationName; + @Field(2) + public LatLonBounds bounds; + @Field(3) + public int maxResults; + @Field(4) + public String locale; + + private GeocodeRequest() { + } + + public GeocodeRequest(String locationName, LatLonBounds bounds) { + this(locationName, bounds, 1); + } + + public GeocodeRequest(String locationName, LatLonBounds bounds, int maxResults) { + this(locationName, bounds, maxResults, Locale.getDefault()); + } + + public GeocodeRequest(String locationName, LatLonBounds bounds, int maxResults, Locale locale) { + this(locationName, bounds, maxResults, locale.toString()); + } + + public GeocodeRequest(String locationName, LatLonBounds bounds, int maxResults, String locale) { + this.locationName = locationName; + this.bounds = bounds; + this.maxResults = maxResults; + this.locale = locale; + } + + public static final Creator CREATOR = new AutoCreator<>(GeocodeRequest.class); +} diff --git a/service-api/src/main/java/org/microg/nlp/service/api/LatLon.java b/service-api/src/main/java/org/microg/nlp/service/api/LatLon.java new file mode 100644 index 0000000..881ca7e --- /dev/null +++ b/service-api/src/main/java/org/microg/nlp/service/api/LatLon.java @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +import android.os.Parcel; +import android.os.Parcelable; + +public class LatLon implements Parcelable { + private double latitude; + private double longitude; + + public LatLon(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + + public double getLatitude() { + return latitude; + } + + public double getLongitude() { + return longitude; + } + + public static final Creator CREATOR = new Creator() { + @Override + public LatLon createFromParcel(Parcel source) { + return new LatLon(source.readDouble(), source.readDouble()); + } + + @Override + public LatLon[] newArray(int size) { + return new LatLon[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeDouble(latitude); + dest.writeDouble(longitude); + } +} diff --git a/service-api/src/main/java/org/microg/nlp/service/api/LatLonBounds.java b/service-api/src/main/java/org/microg/nlp/service/api/LatLonBounds.java new file mode 100644 index 0000000..fe52834 --- /dev/null +++ b/service-api/src/main/java/org/microg/nlp/service/api/LatLonBounds.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class LatLonBounds extends AutoSafeParcelable { + @Field(1) + public LatLon lowerLeft; + @Field(2) + public LatLon upperRight; + + public LatLonBounds(LatLon lowerLeft, LatLon upperRight) { + this.lowerLeft = lowerLeft; + this.upperRight = upperRight; + } + + public static final Creator CREATOR = new AutoCreator<>(LatLonBounds.class); +} diff --git a/service-api/src/main/java/org/microg/nlp/service/api/LocationRequest.java b/service-api/src/main/java/org/microg/nlp/service/api/LocationRequest.java new file mode 100644 index 0000000..1213582 --- /dev/null +++ b/service-api/src/main/java/org/microg/nlp/service/api/LocationRequest.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +import org.microg.safeparcel.AutoSafeParcelable; + +import java.util.UUID; + +public class LocationRequest extends AutoSafeParcelable { + @Field(1) + public ILocationListener listener; + @Field(2) + public long interval; + @Field(3) + public int numUpdates; + @Field(4) + public String id; + + private LocationRequest() { + } + + public LocationRequest(ILocationListener listener, long interval) { + this(listener, interval, Integer.MAX_VALUE, UUID.randomUUID().toString()); + } + + public LocationRequest(ILocationListener listener, long interval, int numUpdates) { + this(listener, interval, numUpdates, UUID.randomUUID().toString()); + } + + public LocationRequest(ILocationListener listener, long interval, int numUpdates, String id) { + this.listener = listener; + this.interval = interval; + this.numUpdates = numUpdates; + this.id = id; + } + + public static final Creator CREATOR = new AutoCreator<>(LocationRequest.class); +} diff --git a/service-api/src/main/java/org/microg/nlp/service/api/ReverseGeocodeRequest.java b/service-api/src/main/java/org/microg/nlp/service/api/ReverseGeocodeRequest.java new file mode 100644 index 0000000..030250c --- /dev/null +++ b/service-api/src/main/java/org/microg/nlp/service/api/ReverseGeocodeRequest.java @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service.api; + +import org.microg.safeparcel.AutoSafeParcelable; + +import java.util.Locale; + +public class ReverseGeocodeRequest extends AutoSafeParcelable { + @Field(1) + public LatLon location; + @Field(2) + public int maxResults; + @Field(3) + public String locale; + + private ReverseGeocodeRequest() { + } + + public ReverseGeocodeRequest(LatLon location) { + this(location, 1); + } + + public ReverseGeocodeRequest(LatLon location, int maxResults) { + this(location, maxResults, Locale.getDefault()); + } + + public ReverseGeocodeRequest(LatLon location, int maxResults, Locale locale) { + this(location, maxResults, locale.toString()); + } + + public ReverseGeocodeRequest(LatLon location, int maxResults, String locale) { + this.location = location; + this.maxResults = maxResults; + this.locale = locale; + } + + public static final Creator CREATOR = new AutoCreator<>(ReverseGeocodeRequest.class); +} diff --git a/service/build.gradle b/service/build.gradle index 28327ef..bf9580a 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -6,7 +6,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'maven-publish' apply plugin: 'signing' @@ -36,9 +35,11 @@ description = 'UnifiedNlp service library' dependencies { implementation project(':api') + implementation project(':service-api') implementation project(':client') 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/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml index 8ebf360..e128fb5 100644 --- a/service/src/main/AndroidManifest.xml +++ b/service/src/main/AndroidManifest.xml @@ -18,13 +18,29 @@ + + + + + + + + + + + + diff --git a/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt b/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt index ad00475..e8828b2 100644 --- a/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt +++ b/service/src/main/kotlin/org/microg/nlp/service/AbstractBackendHelper.kt @@ -14,7 +14,10 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.IBinder import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import kotlinx.coroutines.CoroutineScope +import java.io.PrintWriter import java.security.MessageDigest import java.security.NoSuchAlgorithmException @@ -23,11 +26,13 @@ fun Array?.isNotNullOrEmpty(): Boolean { return this != null && this.isNotEmpty() } -abstract class AbstractBackendHelper(private val TAG: String, private val context: Context, protected val coroutineScope: CoroutineScope, val serviceIntent: Intent, val signatureDigest: String?) : ServiceConnection { +abstract class AbstractBackendHelper(private val TAG: String, private val context: Context, private val lifecycle: Lifecycle, val serviceIntent: Intent, val signatureDigest: String?) : ServiceConnection, LifecycleOwner { private var bound: Boolean = false protected abstract suspend fun close() + override fun getLifecycle(): Lifecycle = lifecycle + protected abstract fun hasBackend(): Boolean override fun onServiceConnected(name: ComponentName, service: IBinder) { @@ -81,6 +86,10 @@ abstract class AbstractBackendHelper(private val TAG: String, private val contex } } + fun dump(writer: PrintWriter?) { + writer?.println(" ${javaClass.simpleName} $serviceIntent bound=$bound") + } + companion object { @Suppress("DEPRECATION") @SuppressLint("PackageManagerGetSignatures") diff --git a/service/src/main/kotlin/org/microg/nlp/service/AsyncGeocoderBackend.kt b/service/src/main/kotlin/org/microg/nlp/service/AsyncGeocoderBackend.kt index 86b6406..e8efe93 100644 --- a/service/src/main/kotlin/org/microg/nlp/service/AsyncGeocoderBackend.kt +++ b/service/src/main/kotlin/org/microg/nlp/service/AsyncGeocoderBackend.kt @@ -14,9 +14,11 @@ import android.os.Looper import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.microg.nlp.api.GeocoderBackend +import java.util.concurrent.* import kotlin.coroutines.suspendCoroutine class AsyncGeocoderBackend(binder: IBinder, name: String = "geocoder-backend-thread") : Thread(name) { + private val syncThreads: ThreadPoolExecutor = ThreadPoolExecutor(1, Runtime.getRuntime().availableProcessors(), 1, TimeUnit.SECONDS, LinkedBlockingQueue()) private lateinit var looper: Looper private lateinit var handler: Handler private val mutex = Mutex(true) @@ -62,6 +64,10 @@ class AsyncGeocoderBackend(binder: IBinder, name: String = "geocoder-backend-thr } } + fun getFromLocationSync(latitude: Double, longitude: Double, maxResults: Int, locale: String?): List
= executeWithTimeout { + backend.getFromLocation(latitude, longitude, maxResults, locale); + } + suspend fun getFromLocationName(locationName: String?, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String?): List
= mutex.withLock { suspendCoroutine { handler.post { @@ -75,6 +81,10 @@ class AsyncGeocoderBackend(binder: IBinder, name: String = "geocoder-backend-thr } } + fun getFromLocationNameSync(locationName: String?, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String?): List
= executeWithTimeout { + backend.getFromLocationName(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale) + } + suspend fun close() { mutex.withLock { suspendCoroutine { @@ -144,6 +154,10 @@ class AsyncGeocoderBackend(binder: IBinder, name: String = "geocoder-backend-thr } } + fun getFromLocationWithOptionsSync(latitude: Double, longitude: Double, maxResults: Int, locale: String?, options: Bundle?): List
= executeWithTimeout { + backend.getFromLocationWithOptions(latitude, longitude, maxResults, locale, options) + } + suspend fun getFromLocationNameWithOptions(locationName: String?, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String?, options: Bundle?): List
= mutex.withLock { suspendCoroutine { handler.post { @@ -156,4 +170,31 @@ class AsyncGeocoderBackend(binder: IBinder, name: String = "geocoder-backend-thr } } } -} \ No newline at end of file + + fun getFromLocationNameWithOptionsSync(locationName: String?, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String?, options: Bundle?): List
= executeWithTimeout { + backend.getFromLocationNameWithOptions(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale, 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 + } +} diff --git a/service/src/main/kotlin/org/microg/nlp/service/GeocodeBackendHelper.kt b/service/src/main/kotlin/org/microg/nlp/service/GeocodeBackendHelper.kt deleted file mode 100644 index aabc3cd..0000000 --- a/service/src/main/kotlin/org/microg/nlp/service/GeocodeBackendHelper.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class GeocodeBackendHelper(context: Context, coroutineScope: CoroutineScope, serviceIntent: Intent, signatureDigest: String?) : AbstractBackendHelper(TAG, context, coroutineScope, serviceIntent, signatureDigest) { - private var backend: AsyncGeocoderBackend? = null - - suspend 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) - } catch (e: Exception) { - Log.w(TAG, e) - unbind() - return emptyList() - } - } - - suspend 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) - } catch (e: Exception) { - Log.w(TAG, e) - unbind() - return emptyList() - } - } - - override fun onServiceConnected(name: ComponentName, service: IBinder) { - super.onServiceConnected(name, service) - backend = AsyncGeocoderBackend(service, name.toShortString() + "-geocoder-backend") - coroutineScope.launch { - 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 suspend 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 index 0568ade..32d73cb 100644 --- a/service/src/main/kotlin/org/microg/nlp/service/GeocodeFuser.kt +++ b/service/src/main/kotlin/org/microg/nlp/service/GeocodeFuser.kt @@ -5,15 +5,25 @@ 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 androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import org.microg.nlp.api.Constants.ACTION_GEOCODER_BACKEND +import java.io.PrintWriter import java.util.ArrayList import java.util.concurrent.CopyOnWriteArrayList -class GeocodeFuser(private val context: Context, private val root: UnifiedLocationServiceRoot) { +private const val TAG = "GeocodeFuser" + +class GeocodeFuser(private val context: Context, private val lifecycle: Lifecycle) : LifecycleOwner { private val backendHelpers = CopyOnWriteArrayList() suspend fun reset() { @@ -25,7 +35,7 @@ class GeocodeFuser(private val context: Context, private val root: UnifiedLocati val intent = Intent(ACTION_GEOCODER_BACKEND) intent.setPackage(parts[0]) intent.setClassName(parts[0], parts[1]) - backendHelpers.add(GeocodeBackendHelper(context, root.coroutineScope, intent, if (parts.size >= 3) parts[2] else null)) + backendHelpers.add(GeocodeBackendHelper(context, lifecycle, intent, if (parts.size >= 3) parts[2] else null)) } } } @@ -47,6 +57,13 @@ class GeocodeFuser(private val context: Context, private val root: UnifiedLocati backendHelpers.clear() } + fun dump(writer: PrintWriter?) { + writer?.println("${backendHelpers.size} backends:") + for (helper in backendHelpers) { + helper?.dump(writer) + } + } + suspend fun getFromLocation(latitude: Double, longitude: Double, maxResults: Int, locale: String): List
? { if (backendHelpers.isEmpty()) return null @@ -58,6 +75,17 @@ class GeocodeFuser(private val context: Context, private val root: UnifiedLocati return result } + fun getFromLocationSync(latitude: Double, longitude: Double, maxResults: Int, locale: String): List
? { + if (backendHelpers.isEmpty()) + return null + val result = ArrayList
() + for (backendHelper in backendHelpers) { + val backendResult = backendHelper.getFromLocationSync(latitude, longitude, maxResults, locale) + result.addAll(backendResult) + } + return result + } + suspend fun getFromLocationName(locationName: String, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String): List
? { if (backendHelpers.isEmpty()) return null @@ -68,4 +96,114 @@ class GeocodeFuser(private val context: Context, private val root: UnifiedLocati } return result } + + fun getFromLocationNameSync(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.getFromLocationNameSync(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale) + result.addAll(backendResult) + } + return result + } + + override fun getLifecycle(): Lifecycle = lifecycle +} + +class GeocodeBackendHelper(context: Context, lifecycle: Lifecycle, serviceIntent: Intent, signatureDigest: String?) : AbstractBackendHelper(TAG, context, lifecycle, serviceIntent, signatureDigest) { + private var backend: AsyncGeocoderBackend? = null + + suspend 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) + } catch (e: Exception) { + Log.w(TAG, e) + unbind() + return emptyList() + } + } + + fun getFromLocationSync(latitude: Double, longitude: Double, maxResults: Int, + locale: String): List
{ + if (backend == null) { + Log.d(TAG, "Not (yet) bound.") + return emptyList() + } + try { + return backend!!.getFromLocationSync(latitude, longitude, maxResults, locale) + } catch (e: Exception) { + Log.w(TAG, e) + lifecycleScope.launch { unbind() } + return emptyList() + } + } + + suspend 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) + } catch (e: Exception) { + Log.w(TAG, e) + unbind() + return emptyList() + } + } + + fun getFromLocationNameSync(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!!.getFromLocationNameSync(locationName, maxResults, lowerLeftLatitude, + lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale) + } catch (e: Exception) { + Log.w(TAG, e) + lifecycleScope.launch { unbind() } + return emptyList() + } + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + super.onServiceConnected(name, service) + backend = AsyncGeocoderBackend(service, name.toShortString() + "-geocoder-backend") + lifecycleScope.launchWhenStarted { + 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 suspend fun close() { + backend!!.close() + } + + public override fun hasBackend(): Boolean { + return backend != null + } } diff --git a/service/src/main/kotlin/org/microg/nlp/service/GeocodeService.kt b/service/src/main/kotlin/org/microg/nlp/service/GeocodeService.kt new file mode 100644 index 0000000..e49324b --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/GeocodeService.kt @@ -0,0 +1,196 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.app.ActivityManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.location.Address +import android.os.Bundle +import android.os.IBinder +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import org.microg.nlp.service.api.* +import org.microg.nlp.service.api.Constants.* +import java.io.FileDescriptor +import java.io.PrintWriter + +private const val TAG = "GeocodeService" + +class GeocodeService : LifecycleService() { + private lateinit var service: GeocodeServiceImpl + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "Creating userspace service...") + service = GeocodeServiceImpl(this, lifecycle) + Log.d(TAG, "Created userspace service.") + } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return service.asBinder() + } + + override fun onDestroy() { + lifecycleScope.launch { + service.destroy() + } + super.onDestroy() + Log.d(TAG, "Destroyed") + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array?) { + service.dump(writer) + } +} + +class GeocodeServiceImpl(private val context: Context, private val lifecycle: Lifecycle) : IGeocodeService.Stub(), LifecycleOwner { + private val fuser = GeocodeFuser(context, lifecycle) + + init { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "Preparing GeocodeFuser...") + fuser.reset() + fuser.bind() + Log.d(TAG, "Finished preparing GeocodeFuser") + } + } + + private fun getCallingPackage(): String? { + val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + val callingPid = getCallingPid() + if (manager != null && callingPid > 0) { + manager.runningAppProcesses.find { it.pid == callingPid }?.pkgList?.firstOrNull()?.let { return it } + } + return context.packageManager.getPackagesForUid(getCallingUid())?.firstOrNull() + } + + private fun processOptions(options: Bundle?): Bundle { + val options = options ?: Bundle() + options.putString("callingPackage", getCallingPackage()) + if (!options.containsKey("packageName")) { + options.putString("packageName", options.getString("packageName")) + } else if (context.checkCallingPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) { + val claimedPackageName = options.getString("packageName") + if (context.packageManager.getPackagesForUid(getCallingUid())?.any { it == claimedPackageName } != true) { + options.putString("packageName", options.getString("packageName")) + } + } + options.putInt("callingUid", getCallingUid()) + options.putInt("callingPid", getCallingPid()) + return options + } + + private fun Bundle.checkPermission(permission: String): Int { + return context.checkPermission(permission, getInt("callingPid"), getInt("callingUid")) + } + + override fun requestGeocode(request: GeocodeRequest?, callback: IAddressesCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (request == null) + return@launchWhenStarted callback.onAddresses(STATUS_INVALID_ARGS, null) + if (extras.checkPermission("android.permission.INTERNET") != PERMISSION_GRANTED) + return@launchWhenStarted callback.onAddresses(STATUS_PERMISSION_ERROR, null) + callback.onAddresses(STATUS_OK, fuser.getFromLocationName( + request.locationName, + request.maxResults, + request.bounds.lowerLeft.latitude, + request.bounds.lowerLeft.longitude, + request.bounds.upperRight.latitude, + request.bounds.upperRight.longitude, + request.locale + )) + } + } + + override fun requestGeocodeSync(request: GeocodeRequest?, options: Bundle?): List
{ + val extras = processOptions(options) + if (request == null || extras.getString("packageName") == null) throw IllegalArgumentException() + if (extras.checkPermission("android.permission.INTERNET") != PERMISSION_GRANTED) throw SecurityException("Missing INTERNET permission") + return fuser.getFromLocationNameSync( + request.locationName, + request.maxResults, + request.bounds.lowerLeft.latitude, + request.bounds.lowerLeft.longitude, + request.bounds.upperRight.latitude, + request.bounds.upperRight.longitude, + request.locale + ).orEmpty() + } + + override fun requestReverseGeocode(request: ReverseGeocodeRequest?, callback: IAddressesCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (request == null) + return@launchWhenStarted callback.onAddresses(STATUS_INVALID_ARGS, null) + if (extras.checkPermission("android.permission.INTERNET") != PERMISSION_GRANTED) + return@launchWhenStarted callback.onAddresses(STATUS_PERMISSION_ERROR, null) + callback.onAddresses(STATUS_OK, fuser.getFromLocation(request.location.latitude, request.location.longitude, request.maxResults, request.locale)) + } + } + + override fun requestReverseGeocodeSync(request: ReverseGeocodeRequest?, options: Bundle?): List
{ + val extras = processOptions(options) + if (request == null || extras.getString("packageName") == null) throw IllegalArgumentException() + if (extras.checkPermission("android.permission.INTERNET") != PERMISSION_GRANTED) throw SecurityException("Missing INTERNET permission") + return fuser.getFromLocationSync(request.location.latitude, request.location.longitude, request.maxResults, request.locale).orEmpty() + } + + override fun reloadPreferences(callback: IStatusCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) + return@launchWhenStarted callback.onStatus(STATUS_PERMISSION_ERROR) + fuser.reset() + fuser.bind() + callback.onStatus(Constants.STATUS_OK) + } + } + + override fun getGeocodeBackends(callback: IStringsCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) + return@launchWhenStarted callback.onStrings(STATUS_PERMISSION_ERROR, null) + callback.onStrings(Constants.STATUS_OK, Preferences(context).geocoderBackends.toList()) + } + } + + override fun setGeocodeBackends(backends: MutableList?, callback: IStatusCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) + return@launchWhenStarted callback.onStatus(STATUS_PERMISSION_ERROR) + if (backends == null) + return@launchWhenStarted callback.onStatus(STATUS_INVALID_ARGS) + Preferences(context).geocoderBackends = backends.toSet() + callback.onStatus(Constants.STATUS_OK) + } + } + + fun dump(writer: PrintWriter?) { + fuser.dump(writer) + } + + suspend fun destroy() { + fuser.destroy() + } + + override fun getLifecycle(): Lifecycle = lifecycle +} + diff --git a/service/src/main/kotlin/org/microg/nlp/service/LocationBackendHelper.kt b/service/src/main/kotlin/org/microg/nlp/service/LocationBackendHelper.kt deleted file mode 100644 index 359a8bb..0000000 --- a/service/src/main/kotlin/org/microg/nlp/service/LocationBackendHelper.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * 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.os.* -import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -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.LocationCallback - -class LocationBackendHelper(context: Context, private val locationFuser: LocationFuser, coroutineScope: CoroutineScope, serviceIntent: Intent, signatureDigest: String?) : AbstractBackendHelper(TAG, context, coroutineScope, serviceIntent, signatureDigest) { - private val callback = Callback() - private var backend: AsyncLocationBackend? = 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. - */ - suspend 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 suspend 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 = AsyncLocationBackend(service, name.toShortString() + "-location-backend") - coroutineScope.launch { - 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 index b5add73..6717518 100644 --- a/service/src/main/kotlin/org/microg/nlp/service/LocationFuser.kt +++ b/service/src/main/kotlin/org/microg/nlp/service/LocationFuser.kt @@ -5,23 +5,31 @@ 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.location.LocationManager +import android.os.* import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope 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 +import org.microg.nlp.api.LocationCallback +import org.microg.nlp.service.api.Constants +import java.io.PrintWriter import java.util.concurrent.CopyOnWriteArrayList -class LocationFuser(private val context: Context, private val root: UnifiedLocationServiceRoot) { +private const val TAG = "LocationFuser" + +class LocationFuser(private val context: Context, private val lifecycle: Lifecycle, private val receiver: LocationReceiver) : LifecycleOwner { private val backendHelpers = CopyOnWriteArrayList() private var fusing = false @@ -38,7 +46,7 @@ class LocationFuser(private val context: Context, private val root: UnifiedLocat val intent = Intent(ACTION_LOCATION_BACKEND) intent.setPackage(parts[0]) intent.setClassName(parts[0], parts[1]) - backendHelpers.add(LocationBackendHelper(context, this, root.coroutineScope, intent, if (parts.size >= 3) parts[2] else null)) + backendHelpers.add(LocationBackendHelper(context, this, lifecycle, intent, if (parts.size >= 3) parts[2] else null)) } } } @@ -84,7 +92,7 @@ class LocationFuser(private val context: Context, private val root: UnifiedLocat if (lastLocationReportTime < location.time) { lastLocationReportTime = location.time Log.v(TAG, "Fused location: $location") - root.reportLocation(location) + receiver.reportLocation(location) } else { Log.v(TAG, "Ignoring location update as it's older than other provider.") } @@ -101,8 +109,8 @@ class LocationFuser(private val context: Context, private val root: UnifiedLocat if (locations[0] == backendResult) continue backendResults.add(backendResult) } - if (!backendResults.isEmpty()) { - location.extras.putParcelableArrayList(LOCATION_EXTRA_OTHER_BACKENDS, backendResults) + if (backendResults.isNotEmpty()) { + location.extras.putParcelableArrayList(Constants.LOCATION_EXTRA_OTHER_BACKENDS, backendResults) } return location } @@ -113,11 +121,20 @@ class LocationFuser(private val context: Context, private val root: UnifiedLocat updateLocation() } - fun getLastLocationForBackend(packageName: String, className: String, signatureDigest: String?): Location? = + 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 + fun dump(writer: PrintWriter?) { + writer?.println("${backendHelpers.size} backends:") + for (helper in backendHelpers) { + helper?.dump(writer) + } + } + + override fun getLifecycle(): Lifecycle = lifecycle + class LocationComparator : Comparator { /** @@ -139,8 +156,116 @@ class LocationFuser(private val context: Context, private val root: UnifiedLocat val SWITCH_ON_FRESHNESS_CLIFF_MS: Long = 30000 // 30 seconds } } +} - companion object { - private val TAG = "UnifiedLocation" + +class LocationBackendHelper(context: Context, private val locationFuser: LocationFuser, lifecycle: Lifecycle, serviceIntent: Intent, signatureDigest: String?) : AbstractBackendHelper(TAG, context, lifecycle, serviceIntent, signatureDigest) { + private val callback = Callback() + private var backend: AsyncLocationBackend? = 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(Constants.LOCATION_EXTRA_BACKEND_PROVIDER, location.provider) + location.extras.putString(Constants.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. + */ + suspend 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 suspend 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 = AsyncLocationBackend(service, name.toShortString() + "-location-backend") + lifecycleScope.launchWhenStarted { + 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() + } } } diff --git a/service/src/main/kotlin/org/microg/nlp/service/LocationService.kt b/service/src/main/kotlin/org/microg/nlp/service/LocationService.kt new file mode 100644 index 0000000..5676083 --- /dev/null +++ b/service/src/main/kotlin/org/microg/nlp/service/LocationService.kt @@ -0,0 +1,348 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.nlp.service + +import android.app.ActivityManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.location.Location +import android.os.Bundle +import android.os.IBinder +import android.os.SystemClock +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import org.microg.nlp.service.api.* +import org.microg.nlp.service.api.Constants.* +import java.io.FileDescriptor +import java.io.PrintWriter +import java.util.* +import kotlin.math.max +import kotlin.math.min + +private const val TAG = "LocationService" +private const val MIN_LOCATION_INTERVAL = 2500L + +class LocationService : LifecycleService() { + private lateinit var service: LocationServiceImpl + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "Creating userspace service...") + service = LocationServiceImpl(this, lifecycle) + Log.d(TAG, "Created userspace service.") + } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return service.asBinder() + } + + override fun onDestroy() { + lifecycleScope.launch { + service.destroy() + } + super.onDestroy() + Log.d(TAG, "Destroyed") + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array?) { + service.dump(writer) + } +} + +interface LocationReceiver { + fun reportLocation(location: Location) +} + +class LocationRequestInternal(private var request: LocationRequest, private val extras: Bundle) { + val id: String + get() = request.id + val callingUid: Int + get() = extras.getInt("callingUid") + val callingPid: Int + get() = extras.getInt("callingPid") + val callingPackage: String? + get() = extras.getString("callingPackage") + val packageName: String + get() = extras.getString("packageName")!! + val interval: Long + get() = request.interval + val numUpdates: Int + get() = request.numUpdates + var updatesDelivered: Int = 0 + private set + val updatesPending: Int + get() = (numUpdates - updatesDelivered).coerceAtLeast(0) + val listener: ILocationListener + get() = request.listener + val source: String + get() = extras.getString("source") ?: "" + + fun report(context: Context, location: Location) { + if (updatesPending <= 0) throw IllegalStateException("Not waiting for updates") + if (context.checkPermission("android.permission.ACCESS_COARSE_LOCATION", callingPid, callingUid) != PERMISSION_GRANTED) throw SecurityException("No permission to access location") + listener.onLocation(STATUS_OK, location) + updatesDelivered++ + } + + fun matches(other: LocationRequestInternal): Boolean { + if (id == other.id && callingPid == other.callingPid) return true + return false + } + + fun adopt(requestInternal: LocationRequestInternal) { + updatesDelivered = 0 + request = requestInternal.request + extras.putAll(requestInternal.extras) + } +} + +class LocationServiceImpl(private val context: Context, private val lifecycle: Lifecycle) : ILocationService.Stub(), LifecycleOwner, LocationReceiver { + private val requests = arrayListOf() + private val fuser = LocationFuser(context, lifecycle, this) + private var lastLocation: Location? = null + private var interval: Long = 0 + private val timer: Timer = Timer("location-requests") + private var timerTask: TimerTask? = null + private var lastTime: Long = 0 + + init { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "Preparing LocationFuser...") + fuser.reset() + fuser.bind() + fuser.update() + Log.d(TAG, "Finished preparing LocationFuser") + } + } + + private fun updateLocationInterval() { + var interval: Long = Long.MAX_VALUE + var requestNow = false + synchronized(requests) { + for (request in requests) { + if (request.interval == 0L && request.updatesPending == 1) requestNow = true + if (request.interval <= 0 || request.updatesPending <= 0) continue + interval = min(interval, request.interval) + } + } + interval = max(interval, MIN_LOCATION_INTERVAL) + + if (this.interval == interval) return + this.interval = interval + + synchronized(timer) { + 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() { + lifecycleScope.launchWhenStarted { + lastTime = SystemClock.elapsedRealtime() + fuser.update() + Log.d(TAG, "Triggered update") + } + } + } + val delay = if (requestNow) { + 0 + } else { + (interval - (SystemClock.elapsedRealtime() - lastTime)).coerceIn(0, interval) + } + timer.scheduleAtFixedRate(timerTask, delay, interval) + this.timerTask = timerTask + } else { + Log.d(TAG, "Disable location updates") + } + } + } + + private fun getCallingPackage(): String? { + val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + val callingPid = getCallingPid() + if (manager != null && callingPid > 0) { + manager.runningAppProcesses.find { it.pid == callingPid }?.pkgList?.firstOrNull()?.let { return it } + } + return context.packageManager.getPackagesForUid(getCallingUid())?.firstOrNull() + } + + private fun processOptions(options: Bundle?): Bundle { + val options = options ?: Bundle() + val callingPackage = getCallingPackage() + options.putString("callingPackage", callingPackage) + if (!options.containsKey("packageName")) { + options.putString("packageName", callingPackage) + } else if (context.checkCallingPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED && context.packageName != callingPackage) { + val claimedPackageName = options.getString("packageName") + if (context.packageManager.getPackagesForUid(getCallingUid())?.any { it == claimedPackageName } != true) { + Log.d(TAG, "$callingPackage invalidly claimed package name $claimedPackageName, ignoring") + options.putString("packageName", callingPackage) + } + } + options.putInt("callingUid", getCallingUid()) + options.putInt("callingPid", getCallingPid()) + return options + } + + private fun Bundle.checkPermission(permission: String): Int { + return context.checkPermission(permission, getInt("callingPid"), getInt("callingUid")) + } + + override fun getLastLocation(listener: ILocationListener?, options: Bundle?) { + val extras = processOptions(options) + if (listener == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (extras.checkPermission("android.permission.ACCESS_COARSE_LOCATION") != PERMISSION_GRANTED) + return@launchWhenStarted listener.onLocation(STATUS_PERMISSION_ERROR, null) + listener.onLocation(STATUS_OK, lastLocation) + } + } + + override fun getLastLocationForBackend(packageName: String?, className: String?, signatureDigest: String?, listener: ILocationListener?, options: Bundle?) { + val extras = processOptions(options) + if (listener == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (extras.checkPermission("android.permission.ACCESS_COARSE_LOCATION") != PERMISSION_GRANTED) + return@launchWhenStarted listener.onLocation(STATUS_PERMISSION_ERROR, null) + if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) + return@launchWhenStarted listener.onLocation(STATUS_PERMISSION_ERROR, null) + listener.onLocation(STATUS_OK, fuser.getLastLocationForBackend(packageName, className, signatureDigest)) + } + } + + override fun updateLocationRequest(request: LocationRequest?, callback: IStatusCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (extras.checkPermission("android.permission.ACCESS_COARSE_LOCATION") != PERMISSION_GRANTED) + return@launchWhenStarted callback.onStatus(STATUS_PERMISSION_ERROR) + if (request == null) + return@launchWhenStarted callback.onStatus(STATUS_INVALID_ARGS) + val requestInternal = LocationRequestInternal(request, extras) + synchronized(requests) { + requests.find { it.matches(requestInternal) }?.adopt(requestInternal) ?: requests.add(requestInternal) + } + updateLocationInterval() + callback.onStatus(STATUS_OK) + } + } + + override fun cancelLocationRequestByListener(listener: ILocationListener?, callback: IStatusCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (listener == null) + return@launchWhenStarted callback.onStatus(STATUS_INVALID_ARGS) + synchronized(requests) { + requests.removeAll { it.listener == listener } + } + updateLocationInterval() + callback.onStatus(STATUS_OK) + } + } + + override fun cancelLocationRequestById(id: String?, callback: IStatusCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (id == null) + return@launchWhenStarted callback.onStatus(STATUS_INVALID_ARGS) + synchronized(requests) { + requests.removeAll { it.id == id && it.callingPid == extras.getInt("callingPid") } + } + updateLocationInterval() + callback.onStatus(STATUS_OK) + } + } + + override fun forceLocationUpdate(callback: IStatusCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) + return@launchWhenStarted callback.onStatus(STATUS_PERMISSION_ERROR) + fuser.update() + callback.onStatus(STATUS_OK) + } + } + + override fun reloadPreferences(callback: IStatusCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) + return@launchWhenStarted callback.onStatus(STATUS_PERMISSION_ERROR) + fuser.reset() + fuser.bind() + callback.onStatus(STATUS_OK) + } + } + + override fun getLocationBackends(callback: IStringsCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) + return@launchWhenStarted callback.onStrings(STATUS_PERMISSION_ERROR, null) + callback.onStrings(STATUS_OK, Preferences(context).locationBackends.toList()) + } + } + + override fun setLocationBackends(backends: MutableList?, callback: IStatusCallback?, options: Bundle?) { + val extras = processOptions(options) + if (callback == null || extras.getString("packageName") == null) return + lifecycleScope.launchWhenStarted { + if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) + return@launchWhenStarted callback.onStatus(STATUS_PERMISSION_ERROR) + if (backends == null) + return@launchWhenStarted callback.onStatus(STATUS_INVALID_ARGS) + Preferences(context).locationBackends = backends.toSet() + callback.onStatus(STATUS_OK) + } + } + + override fun reportLocation(location: Location) { + this.lastLocation = location + val requestsToDelete = hashSetOf() + synchronized(requests) { + for (request in requests) { + try { + request.report(context, location) + if (request.updatesPending <= 0) requestsToDelete.add(request) + } catch (e: Exception) { + Log.w(TAG, "Removing request due to error: ", e) + requestsToDelete.add(request) + } + } + requests.removeAll(requestsToDelete) + } + updateLocationInterval() + } + + fun dump(writer: PrintWriter?) { + writer?.println("Last reported location: $lastLocation") + writer?.println("Current location interval: $interval") + writer?.println("${requests.size} requests:") + for (request in requests) { + writer?.println(" ${request.id} package=${request.packageName} source=${request.source} interval=${request.interval} pending=${request.updatesPending}") + } + fuser.dump(writer) + } + + suspend fun destroy() { + fuser.destroy() + } + + override fun getLifecycle(): Lifecycle = lifecycle +} diff --git a/service/src/main/kotlin/org/microg/nlp/service/PackageChangedReceiver.kt b/service/src/main/kotlin/org/microg/nlp/service/PackageChangedReceiver.kt index ca8c2ad..fcfc5c9 100644 --- a/service/src/main/kotlin/org/microg/nlp/service/PackageChangedReceiver.kt +++ b/service/src/main/kotlin/org/microg/nlp/service/PackageChangedReceiver.kt @@ -11,8 +11,6 @@ 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) { @@ -29,14 +27,14 @@ class PackageChangedReceiver : BroadcastReceiver() { for (backend in preferences.locationBackends) { if (backend.startsWith("$packageName/")) { Log.d(TAG, "Reloading location service for $packageName") - suspend { UnifiedLocationClient[context].reloadPreferences() } + UnifiedLocationServiceEntryPoint.reloadPreferences() return } } for (backend in preferences.geocoderBackends) { if (backend.startsWith("$packageName/")) { Log.d(TAG, "Reloading geocoding service for $packageName") - suspend { UnifiedLocationClient[context].reloadPreferences() } + UnifiedLocationServiceEntryPoint.reloadPreferences() return } } diff --git a/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceEntryPoint.kt b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceEntryPoint.kt index 789c088..9823ad2 100644 --- a/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceEntryPoint.kt +++ b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceEntryPoint.kt @@ -5,48 +5,49 @@ package org.microg.nlp.service -import android.app.Service import android.content.Intent import android.os.IBinder import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import java.io.FileDescriptor +import java.io.PrintWriter -class UnifiedLocationServiceEntryPoint : Service() { - private var root: UnifiedLocationServiceRoot? = null - - @Synchronized - fun destroy() { - if (root != null) { - root!!.destroy() - root = null - } - } +@Deprecated("Use LocationService or GeocodeService") +class UnifiedLocationServiceEntryPoint : LifecycleService() { + private var root: UnifiedLocationServiceRoot = UnifiedLocationServiceRoot(this, lifecycle) override fun onCreate() { + singleton = this super.onCreate() Log.d(TAG, "onCreate") - destroy() + lifecycleScope.launchWhenStarted { root.reset() } } override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) Log.d(TAG, "onBind: $intent") - synchronized(this) { - if (root == null) { - root = UnifiedLocationServiceRoot(this, CoroutineScope(Dispatchers.IO + Job())) - } - return root!!.asBinder() - } + return root.asBinder() } override fun onDestroy() { + super.onDestroy() Log.d(TAG, "onDestroy") - destroy() + root.destroy() + singleton = null + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array?) { + writer?.println("Singleton: ${singleton != null}") + root.dump(writer) } companion object { private val TAG = "ULocService" + private var singleton: UnifiedLocationServiceEntryPoint? = null + + fun reloadPreferences() { + singleton?.root?.reloadPreferences() + } } } diff --git a/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceInstance.kt b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceInstance.kt index 64c3050..d0457c3 100644 --- a/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceInstance.kt +++ b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceInstance.kt @@ -14,14 +14,13 @@ import android.os.Binder.getCallingUid import android.os.Bundle import android.os.RemoteException import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import androidx.lifecycle.lifecycleScope import org.microg.nlp.client.UnifiedLocationClient.Companion.KEY_FORCE_NEXT_UPDATE import org.microg.nlp.client.UnifiedLocationClient.Companion.KEY_OP_PACKAGE_NAME import org.microg.nlp.client.UnifiedLocationClient.Companion.PERMISSION_SERVICE_ADMIN +import java.io.PrintWriter +@Deprecated("Use LocationService or GeocodeService") class UnifiedLocationServiceInstance(private val root: UnifiedLocationServiceRoot) : UnifiedLocationService.Default() { private var callback: LocationCallback? = null private var interval: Long = 0 @@ -88,7 +87,7 @@ class UnifiedLocationServiceInstance(private val root: UnifiedLocationServiceRoo if (lastLocation == null || lastLocation.time < System.currentTimeMillis() - UnifiedLocationServiceRoot.MAX_LOCATION_AGE || options.getBoolean(KEY_FORCE_NEXT_UPDATE, false)) { Log.d(TAG, "requestSingleUpdate[$debugPackageString] requesting new location") singleUpdatePending = true - root.coroutineScope.launch { + root.lifecycleScope.launchWhenStarted { root.locationFuser.update() root.updateLocationInterval() } @@ -103,6 +102,10 @@ class UnifiedLocationServiceInstance(private val root: UnifiedLocationServiceRoo } } + fun dump(writer: PrintWriter?) { + writer?.println("$debugPackageString: interval $interval, single $singleUpdatePending") + } + 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 index adb14ee..0c7ed52 100644 --- a/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceRoot.kt +++ b/service/src/main/kotlin/org/microg/nlp/service/UnifiedLocationServiceRoot.kt @@ -13,9 +13,11 @@ import android.os.Binder import android.os.Bundle import android.os.Process import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import org.microg.nlp.client.UnifiedLocationClient.Companion.PERMISSION_SERVICE_ADMIN +import java.io.PrintWriter import java.util.* import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ThreadPoolExecutor @@ -25,14 +27,15 @@ import kotlin.collections.set import kotlin.math.max import kotlin.math.min -class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntryPoint, val coroutineScope: CoroutineScope) : UnifiedLocationService.Stub() { +@Deprecated("Use LocationService or GeocodeService") +class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntryPoint, private val lifecycle: Lifecycle) : UnifiedLocationService.Stub(), LifecycleOwner, LocationReceiver { 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, this) + val locationFuser: LocationFuser = LocationFuser(service, lifecycle, this) + val geocodeFuser: GeocodeFuser = GeocodeFuser(service, lifecycle) var lastReportedLocation: Location? = null private set private var interval: Long = 0 @@ -40,10 +43,6 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr val context: Context get() = service - init { - coroutineScope.launch { reset() } - } - val instance: UnifiedLocationServiceInstance @Synchronized get() { checkLocationPermission() @@ -53,7 +52,7 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr } @Synchronized - fun reportLocation(location: Location) { + override fun reportLocation(location: Location) { for (instance in ArrayList(instances.values)) { instance.reportLocation(location) } @@ -72,7 +71,7 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr } fun destroy() { - coroutineScope.launch { + lifecycleScope.launchWhenStarted { locationFuser.destroy() geocodeFuser.destroy() } @@ -102,7 +101,7 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr val timerTask = object : TimerTask() { override fun run() { - coroutineScope.launch { + lifecycleScope.launchWhenStarted { lastTime = System.currentTimeMillis() locationFuser.update() } @@ -138,7 +137,7 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr } override fun getFromLocationWithOptions(latitude: Double, longitude: Double, maxResults: Int, locale: String, options: Bundle, callback: AddressCallback) { - coroutineScope.launch { + lifecycleScope.launchWhenStarted { val res = try { geocodeFuser.getFromLocation(latitude, longitude, maxResults, locale).orEmpty() } catch (e: Exception) { @@ -154,7 +153,7 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr } override fun getFromLocationNameWithOptions(locationName: String, maxResults: Int, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, locale: String, options: Bundle, callback: AddressCallback) { - coroutineScope.launch { + lifecycleScope.launchWhenStarted { val res = try { geocodeFuser.getFromLocationName(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale).orEmpty() } catch (e: Exception) { @@ -198,7 +197,7 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr override fun reloadPreferences() { checkAdminPermission(); - coroutineScope.launch { + lifecycleScope.launchWhenStarted { reset() } } @@ -213,7 +212,8 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr return locationFuser.getLastLocationForBackend(packageName, className, signatureDigest) } - @Synchronized + override fun getLifecycle(): Lifecycle = lifecycle + suspend fun reset() { locationFuser.reset() locationFuser.bind() @@ -221,9 +221,19 @@ class UnifiedLocationServiceRoot(private val service: UnifiedLocationServiceEntr geocodeFuser.bind() } + fun dump(writer: PrintWriter?) { + writer?.println("Last reported location: $lastReportedLocation") + writer?.println("Current location interval: $interval") + for (instance in instances.values) { + instance.dump(writer) + } + locationFuser.dump(writer) + geocodeFuser.dump(writer) + } + companion object { private val TAG = "ULocService" val MIN_LOCATION_INTERVAL = 2500L - val MAX_LOCATION_AGE = 3600000L + val MAX_LOCATION_AGE = 300000L } } diff --git a/settings.gradle b/settings.gradle index 9581e9d..34ec6bb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,6 +5,7 @@ include ':api' include ':service' +include ':service-api' include ':compat' include ':client' include ':location-v1'