From f5ad796b827de2993153c4459e582c4c9037f7b6 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 18 Apr 2023 14:51:07 +0200 Subject: [PATCH] Location: Entire rewrite of the location subsystem No modules anymore, they became unreliable in recent Android versions due to "energy savings". --- build.gradle | 11 +- .../common/internal/GetServiceRequest.java | 5 +- .../main/java/org/microg/gms/common/Hide.java | 19 + .../java/org/microg/gms/common/PublicApi.java | 2 + play-services-core/build.gradle | 13 - .../org/microg/gms/ui/SettingsFragment.kt | 16 +- .../src/main/res/navigation/nav_settings.xml | 4 - .../src/main/res/xml/preferences_start.xml | 6 +- play-services-location/core/build.gradle | 16 +- .../core/src/main/AndroidManifest.xml | 57 +- .../gms/location/GoogleLocationManager.java | 299 --- .../GoogleLocationManagerService.java | 65 - .../GoogleLocationManagerServiceImpl.java | 436 ---- .../gms/location/LocationChangeListener.java | 21 - .../gms/location/LocationRequestHelper.java | 266 --- .../gms/location/MockLocationProvider.java | 49 - .../gms/location/RealLocationProvider.java | 167 -- .../gms/location/ReportingAndroidService.java | 41 - .../gms/location/ReportingServiceImpl.java | 103 - .../gms/location/UnifiedLocationProvider.kt | 141 -- .../org/microg/gms/location/extensions.kt | 32 + .../AbstractLocationManagerInstance.kt | 236 +++ .../manager/DeviceOrientationManager.kt | 313 +++ .../location/manager/LastLocationCapsule.kt | 144 ++ .../gms/location/manager/LocationManager.kt | 253 +++ .../manager/LocationManagerInstance.kt | 318 +++ .../manager/LocationManagerService.kt | 61 + .../location/manager/LocationPostProcessor.kt | 147 ++ .../manager/LocationRequestManager.kt | 335 +++ .../microg/gms/location/manager/extensions.kt | 219 ++ .../location/network/LocationCacheDatabase.kt | 206 ++ .../network/NetworkLocationRequest.kt | 28 + .../network/NetworkLocationService.kt | 291 +++ .../gms/location/network/cell/CellDetails.kt | 30 + .../network/cell/CellDetailsCallback.kt | 10 + .../network/cell/CellDetailsSource.kt | 50 + .../gms/location/network/cell/extensions.kt | 164 ++ .../microg/gms/location/network/extensions.kt | 23 + .../gms/location/network/mozilla/CellTower.kt | 45 + .../gms/location/network/mozilla/Fallback.kt | 20 + .../network/mozilla/GeolocateRequest.kt | 32 + .../network/mozilla/GeolocateResponse.kt | 13 + .../mozilla/MozillaLocationServiceClient.kt | 86 + .../gms/location/network/mozilla/RadioType.kt | 14 + .../location/network/mozilla/ResponseError.kt | 11 + .../network/mozilla/ResponseLocation.kt | 11 + .../network/mozilla/ServiceException.kt | 8 + .../network/mozilla/WifiAccessPoint.kt | 41 + .../location/network/mozilla/extensions.kt | 91 + .../gms/location/network/wifi/WifiDetails.kt | 15 + .../network/wifi/WifiDetailsCallback.kt | 10 + .../network/wifi/WifiDetailsSource.kt | 22 + .../network/wifi/WifiManagerSource.kt | 42 + .../network/wifi/WifiScannerSource.kt | 46 + .../gms/location/network/wifi/extensions.kt | 59 + .../AbstractLocationProviderPreTiramisu.kt | 71 + .../provider/GenericLocationProvider.kt | 18 + .../provider/GeocodeProviderService.kt | 23 + .../NetworkLocationProviderPreTiramisu.kt | 124 ++ .../NetworkLocationProviderService.kt | 74 + .../OpenStreetMapNominatimGeocodeProvider.kt | 177 ++ .../gms/location/provider/extensions.kt | 28 + .../reporting/ReportingAndroidService.kt | 28 + .../reporting/ReportingServiceInstance.kt | 64 + .../gms/location/reporting/extensions.kt | 8 + .../location/ActivityRecognitionRequest.aidl | 3 + .../location/ActivityTransitionRequest.aidl | 3 + .../gms/location/CurrentLocationRequest.aidl | 3 + .../location/IDeviceOrientationListener.aidl | 2 +- .../gms/location/ILocationCallback.aidl | 5 +- .../gms/location/ILocationListener.aidl | 3 +- .../gms/location/LastLocationRequest.aidl | 3 + .../location/LocationAvailabilityRequest.aidl | 3 + .../gms/location/SleepSegmentRequest.aidl | 3 + .../IFusedLocationProviderCallback.aidl | 1 + .../internal/IGeofencerCallbacks.aidl | 6 +- .../IGoogleLocationManagerService.aidl | 116 +- .../ILocationAvailabilityStatusCallback.aidl | 8 + .../internal/ILocationStatusCallback.aidl | 8 + .../location/internal/LocationReceiver.aidl | 3 + .../gms/location/ActivityRecognition.java | 2 + .../location/ActivityRecognitionClient.java | 30 +- .../location/ActivityRecognitionRequest.java | 40 + .../gms/location/ActivityTransition.java | 122 ++ .../gms/location/ActivityTransitionEvent.java | 91 + .../location/ActivityTransitionRequest.java | 89 + .../location/ActivityTransitionResult.java | 91 + .../gms/location/CurrentLocationRequest.java | 269 +++ .../gms/location/DetectedActivity.java | 43 +- .../gms/location/DeviceOrientation.java | 86 +- .../location/DeviceOrientationRequest.java | 14 +- .../location/FusedLocationProviderApi.java | 157 ++ .../location/FusedLocationProviderClient.java | 107 +- .../google/android/gms/location/Geofence.java | 91 +- .../gms/location/GeofenceStatusCodes.java | 47 +- .../android/gms/location/GeofencingApi.java | 11 +- .../gms/location/GeofencingClient.java | 19 + .../android/gms/location/GeofencingEvent.java | 31 +- .../gms/location/GeofencingRequest.java | 166 +- .../android/gms/location/GestureRequest.java | 2 + .../gms/location/LastLocationRequest.java | 163 ++ .../gms/location/LocationAvailability.java | 95 +- .../location/LocationAvailabilityRequest.java | 21 + .../android/gms/location/LocationClient.java | 10 + .../gms/location/LocationListener.java | 3 +- .../android/gms/location/LocationRequest.java | 2 +- .../LocationSettingsConfiguration.java | 2 + .../location/LocationSettingsResponse.java | 40 + .../android/gms/location/LocationStatus.java | 2 + .../gms/location/NetworkLocationStatus.java | 33 + .../android/gms/location/SettingsApi.java | 2 + .../gms/location/ThrottleBehavior.java | 2 + .../gms/location/internal/ClientIdentity.java | 45 + .../DeviceOrientationRequestInternal.java | 2 + .../DeviceOrientationRequestUpdateData.java | 2 + .../internal/FusedLocationProviderResult.java | 2 + .../location/internal/LocationReceiver.java | 112 + .../internal/LocationRequestInternal.java | 11 +- .../internal/LocationRequestUpdateData.java | 19 +- .../location/internal/ParcelableGeofence.java | 81 +- .../FusedLocationProviderClientImpl.java | 41 +- .../gms/location/GeofencingClientImpl.java | 24 + .../gms/location/LocationClientImpl.java | 15 + .../system-api/build.gradle | 21 + .../system-api/src/main/AndroidManifest.xml | 6 + .../java/android/location/GeocoderParams.java | 84 + .../main/java/android/location/Geofence.java | 172 ++ .../main/java/android/location/Location.java | 555 +++++ .../android/location/LocationManager.java | 1052 ++++++++++ .../android/location/LocationRequest.java | 472 +++++ .../java/android/net/wifi/WifiScanner.java | 1812 +++++++++++++++++ .../internal/location/ProviderProperties.java | 124 ++ .../internal/location/ProviderRequest.java | 62 + .../location/provider/GeocodeProvider.java | 54 + .../location/provider/LocationProvider.java | 232 +++ .../provider/LocationProviderBase.java | 276 +++ .../provider/LocationRequestUnbundled.java | 107 + .../provider/ProviderPropertiesUnbundled.java | 29 + .../provider/ProviderRequestUnbundled.java | 103 + .../location/WearableLocationService.java | 19 +- settings.gradle | 1 + 141 files changed, 11420 insertions(+), 1946 deletions(-) create mode 100644 play-services-basement/src/main/java/org/microg/gms/common/Hide.java delete mode 100644 play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManager.java delete mode 100644 play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManagerService.java delete mode 100644 play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManagerServiceImpl.java delete mode 100644 play-services-location/core/src/main/java/org/microg/gms/location/LocationChangeListener.java delete mode 100644 play-services-location/core/src/main/java/org/microg/gms/location/LocationRequestHelper.java delete mode 100644 play-services-location/core/src/main/java/org/microg/gms/location/MockLocationProvider.java delete mode 100644 play-services-location/core/src/main/java/org/microg/gms/location/RealLocationProvider.java delete mode 100644 play-services-location/core/src/main/java/org/microg/gms/location/ReportingAndroidService.java delete mode 100644 play-services-location/core/src/main/java/org/microg/gms/location/ReportingServiceImpl.java delete mode 100644 play-services-location/core/src/main/java/org/microg/gms/location/UnifiedLocationProvider.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/extensions.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/AbstractLocationManagerInstance.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LastLocationCapsule.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerInstance.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerService.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationPostProcessor.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/LocationCacheDatabase.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsCallback.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/extensions.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/CellTower.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/Fallback.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateRequest.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateResponse.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/RadioType.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseError.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseLocation.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ServiceException.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/WifiAccessPoint.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/extensions.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsSource.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/extensions.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingAndroidService.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingServiceInstance.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/extensions.kt create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/ActivityRecognitionRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/ActivityTransitionRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/CurrentLocationRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/LastLocationRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/LocationAvailabilityRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/SleepSegmentRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/ILocationAvailabilityStatusCallback.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/ILocationStatusCallback.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/LocationReceiver.aidl create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/ActivityTransition.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionEvent.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionResult.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/CurrentLocationRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LastLocationRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationAvailabilityRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsResponse.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/NetworkLocationStatus.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/internal/ClientIdentity.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/internal/LocationReceiver.java create mode 100644 play-services-location/system-api/build.gradle create mode 100644 play-services-location/system-api/src/main/AndroidManifest.xml create mode 100644 play-services-location/system-api/src/main/java/android/location/GeocoderParams.java create mode 100644 play-services-location/system-api/src/main/java/android/location/Geofence.java create mode 100644 play-services-location/system-api/src/main/java/android/location/Location.java create mode 100644 play-services-location/system-api/src/main/java/android/location/LocationManager.java create mode 100644 play-services-location/system-api/src/main/java/android/location/LocationRequest.java create mode 100644 play-services-location/system-api/src/main/java/android/net/wifi/WifiScanner.java create mode 100644 play-services-location/system-api/src/main/java/com/android/internal/location/ProviderProperties.java create mode 100644 play-services-location/system-api/src/main/java/com/android/internal/location/ProviderRequest.java create mode 100644 play-services-location/system-api/src/main/java/com/android/location/provider/GeocodeProvider.java create mode 100644 play-services-location/system-api/src/main/java/com/android/location/provider/LocationProvider.java create mode 100644 play-services-location/system-api/src/main/java/com/android/location/provider/LocationProviderBase.java create mode 100644 play-services-location/system-api/src/main/java/com/android/location/provider/LocationRequestUnbundled.java create mode 100644 play-services-location/system-api/src/main/java/com/android/location/provider/ProviderPropertiesUnbundled.java create mode 100644 play-services-location/system-api/src/main/java/com/android/location/provider/ProviderRequestUnbundled.java diff --git a/build.gradle b/build.gradle index 2be090324f..ff1a1eb9b7 100644 --- a/build.gradle +++ b/build.gradle @@ -5,18 +5,17 @@ buildscript { ext.cronetVersion = '102.5005.125' - ext.nlpVersion = '2.0-alpha10' - ext.safeParcelVersion = '1.7.0' + ext.safeParcelVersion = '1.7.1' ext.wearableVersion = '0.1.1' ext.kotlinVersion = '1.7.10' ext.coroutineVersion = '1.6.4' ext.annotationVersion = '1.5.0' - ext.appcompatVersion = '1.4.2' + ext.appcompatVersion = '1.6.1' ext.biometricVersion = '1.1.0' - ext.coreVersion = '1.8.0' - ext.fragmentVersion = '1.5.1' + ext.coreVersion = '1.9.0' + ext.fragmentVersion = '1.5.5' ext.lifecycleVersion = '2.5.1' ext.loaderVersion = '1.1.0' ext.mediarouterVersion = '1.3.1' @@ -36,7 +35,7 @@ buildscript { ext.androidMinSdk = 14 ext.androidTargetSdk = 29 - ext.androidCompileSdk = 31 + ext.androidCompileSdk = 33 repositories { mavenCentral() diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java b/play-services-basement/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java index 9b7c594e88..0b1cc52f91 100644 --- a/play-services-basement/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java +++ b/play-services-basement/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java @@ -32,7 +32,7 @@ public class GetServiceRequest extends AutoSafeParcelable { @Field(1) - private int versionCode = 4; + private int versionCode = 6; @Field(2) public final int serviceId; @Field(3) @@ -48,6 +48,7 @@ public class GetServiceRequest extends AutoSafeParcelable { @Field(8) public Account account; @Field(9) + @Deprecated private long field9; @Field(10) public Feature[] defaultFeatures; @@ -60,7 +61,7 @@ public class GetServiceRequest extends AutoSafeParcelable { @Field(14) private boolean field14; @Field(15) - private String field15; + private String attributionTag; private GetServiceRequest() { serviceId = -1; diff --git a/play-services-basement/src/main/java/org/microg/gms/common/Hide.java b/play-services-basement/src/main/java/org/microg/gms/common/Hide.java new file mode 100644 index 0000000000..e15414b5ad --- /dev/null +++ b/play-services-basement/src/main/java/org/microg/gms/common/Hide.java @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Hide the class, method or field from the public API. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR}) +public @interface Hide { +} diff --git a/play-services-basement/src/main/java/org/microg/gms/common/PublicApi.java b/play-services-basement/src/main/java/org/microg/gms/common/PublicApi.java index 9266397967..a7fb9b447f 100644 --- a/play-services-basement/src/main/java/org/microg/gms/common/PublicApi.java +++ b/play-services-basement/src/main/java/org/microg/gms/common/PublicApi.java @@ -43,6 +43,8 @@ * marked as public api. * * @return true if the method or field is not part of the public api + * @deprecated use {@link Hide} instead */ + @Deprecated boolean exclude() default false; } diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 76920708b9..1ed642bb10 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -54,19 +54,6 @@ dependencies { implementation project(':play-services-safetynet') implementation project(':play-services-tasks-ktx') - runtimeOnly("org.microg.nlp:geocode-v1:$nlpVersion") { - exclude group: 'org.microg', module: 'safe-parcel' - } - runtimeOnly("org.microg.nlp:location-v2:$nlpVersion") { - exclude group: 'org.microg', module: 'safe-parcel' - } - runtimeOnly("org.microg.nlp:location-v3:$nlpVersion") { - exclude group: 'org.microg', module: 'safe-parcel' - } - implementation("org.microg.nlp:ui:$nlpVersion") { - exclude group: 'org.microg', module: 'safe-parcel' - } - withMapboxImplementation project(':play-services-maps-core-mapbox') withVtmImplementation project(':play-services-maps-core-vtm') diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt index 196cee331d..63e47ca838 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt @@ -15,9 +15,6 @@ import org.microg.gms.checkin.CheckinPrefs import org.microg.gms.gcm.GcmDatabase import org.microg.gms.gcm.getGcmServiceInfo import org.microg.gms.safetynet.SafetyNetPreferences -import org.microg.nlp.client.GeocodeClient -import org.microg.nlp.client.LocationClient -import org.microg.nlp.client.UnifiedLocationClient import org.microg.tools.ui.ResourceSettingsFragment class SettingsFragment : ResourceSettingsFragment() { @@ -36,8 +33,8 @@ class SettingsFragment : ResourceSettingsFragment() { findNavController().navigate(requireContext(), R.id.openSafetyNetSettings) true } - findPreference(PREF_UNIFIEDNLP)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - findNavController().navigate(requireContext(), R.id.openUnifiedNlpSettings) + findPreference(PREF_LOCATION)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + // TODO true } findPreference(PREF_EXPOSURE)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -73,13 +70,6 @@ class SettingsFragment : ResourceSettingsFragment() { findPreference(PREF_CHECKIN)!!.setSummary(if (CheckinPrefs.isEnabled(context)) R.string.service_status_enabled_short else R.string.service_status_disabled_short) findPreference(PREF_SNET)!!.setSummary(if (SafetyNetPreferences.isEnabled(context)) R.string.service_status_enabled_short else R.string.service_status_disabled_short) - val backendCount = try { - LocationClient(context, lifecycle).getLocationBackends().size + GeocodeClient(context, lifecycle).getGeocodeBackends().size - } catch (e: Exception) { - 0 - } - findPreference(PREF_UNIFIEDNLP)!!.summary = context.resources.getQuantityString(R.plurals.pref_unifiednlp_summary, backendCount, backendCount) - findPreference(PREF_EXPOSURE)?.isVisible = NearbyPreferencesIntegration.isAvailable findPreference(PREF_EXPOSURE)?.icon = NearbyPreferencesIntegration.getIcon(context) findPreference(PREF_EXPOSURE)?.summary = NearbyPreferencesIntegration.getExposurePreferenceSummary(context) @@ -89,7 +79,7 @@ class SettingsFragment : ResourceSettingsFragment() { const val PREF_ABOUT = "pref_about" const val PREF_GCM = "pref_gcm" const val PREF_SNET = "pref_snet" - const val PREF_UNIFIEDNLP = "pref_unifiednlp" + const val PREF_LOCATION = "pref_location" const val PREF_CHECKIN = "pref_checkin" const val PREF_EXPOSURE = "pref_exposure" } diff --git a/play-services-core/src/main/res/navigation/nav_settings.xml b/play-services-core/src/main/res/navigation/nav_settings.xml index 4f4d188248..4ad2b9c499 100644 --- a/play-services-core/src/main/res/navigation/nav_settings.xml +++ b/play-services-core/src/main/res/navigation/nav_settings.xml @@ -11,9 +11,6 @@ android:id="@+id/settingsFragment" android:name="org.microg.gms.ui.SettingsFragment" android:label="@string/gms_settings_name"> - @@ -154,6 +151,5 @@ - diff --git a/play-services-core/src/main/res/xml/preferences_start.xml b/play-services-core/src/main/res/xml/preferences_start.xml index 1ff7fb6098..2245ea339a 100644 --- a/play-services-core/src/main/res/xml/preferences_start.xml +++ b/play-services-core/src/main/res/xml/preferences_start.xml @@ -60,8 +60,10 @@ + android:key="pref_location" + android:title="@string/prefcat_location_service" + android:enabled="false" + app:isPreferenceVisible="false"/> - - + + + + + + + + + + + + + + + - + - + @@ -21,5 +45,28 @@ + + + + + + + + + + + + + + + diff --git a/play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManager.java b/play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManager.java deleted file mode 100644 index 7b665958f1..0000000000 --- a/play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManager.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.location; - -import android.Manifest; -import android.app.PendingIntent; -import android.content.Context; -import android.location.Location; -import android.location.LocationManager; -import android.os.Binder; -import android.os.Handler; -import android.os.Looper; -import android.os.RemoteException; -import android.util.Log; - -import com.google.android.gms.common.api.Status; -import com.google.android.gms.location.ILocationListener; -import com.google.android.gms.location.LocationRequest; -import com.google.android.gms.location.internal.FusedLocationProviderResult; -import com.google.android.gms.location.internal.LocationRequestUpdateData; - -import org.microg.gms.common.PackageUtils; -import org.microg.gms.common.Utils; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -import static android.Manifest.permission.ACCESS_COARSE_LOCATION; -import static android.Manifest.permission.ACCESS_FINE_LOCATION; -import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static android.location.LocationManager.GPS_PROVIDER; -import static com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY; -import static com.google.android.gms.location.Priority.PRIORITY_PASSIVE; - -import androidx.lifecycle.Lifecycle; - -public class GoogleLocationManager implements LocationChangeListener { - private static final String TAG = "LocationManager"; - private static final String MOCK_PROVIDER = "mock"; - private static final long VERIFY_CURRENT_REQUESTS_INTERVAL_MS = 5000; // 5 seconds - private static final long SWITCH_ON_FRESHNESS_CLIFF_MS = 30000; // 30 seconds - private static final String ACCESS_MOCK_LOCATION = "android.permission.ACCESS_MOCK_LOCATION"; - - private final Context context; - private final Handler handler; - private final Runnable verifyCurrentRequestsRunnable = this::verifyCurrentRequests; - private final RealLocationProvider gpsProvider; - private final UnifiedLocationProvider networkProvider; - private final MockLocationProvider mockProvider; - private final List currentRequests = new ArrayList(); - - public GoogleLocationManager(Context context, Lifecycle lifecycle) { - long callingIdentity = Binder.clearCallingIdentity(); - this.context = context; - LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - if (Utils.hasSelfPermissionOrNotify(context, Manifest.permission.ACCESS_FINE_LOCATION)) { - this.gpsProvider = new RealLocationProvider(locationManager, GPS_PROVIDER, this); - } else { - this.gpsProvider = null; - } - if (Utils.hasSelfPermissionOrNotify(context, Manifest.permission.ACCESS_COARSE_LOCATION)) { - this.networkProvider = new UnifiedLocationProvider(context, this, lifecycle); - } else { - this.networkProvider = null; - } - mockProvider = new MockLocationProvider(this); - handler = new Handler(Looper.getMainLooper()); - Binder.restoreCallingIdentity(callingIdentity); - } - - public void invokeOnceReady(Runnable runnable) { - Runnable networkRunnable = () -> { - if (networkProvider != null) { - networkProvider.invokeOnceReady(runnable); - } else { - runnable.run(); - } - }; - if (gpsProvider != null) { - gpsProvider.invokeOnceReady(networkRunnable); - } else { - networkRunnable.run(); - } - } - - public Location getLastLocation(String packageName) { - return getLocation(hasFineLocationPermission(), hasCoarseLocationPermission()); - } - - public Location getLocation(boolean gpsPermission, boolean networkPermission) { - if (mockProvider.getLocation() != null) - return mockProvider.getLocation(); - if (gpsPermission) { - Location network = networkProvider == null ? null : networkProvider.getLastLocation(); - Location gps = gpsProvider == null ? null : gpsProvider.getLastLocation(); - if (network == null) - return gps; - if (gps == null) - return network; - if (gps.getTime() > network.getTime() - SWITCH_ON_FRESHNESS_CLIFF_MS) - return gps; - return network; - } else if (networkPermission) { - Location network = networkProvider == null ? null : networkProvider.getLastLocation(); - if (network != null && network.getExtras() != null && network.getExtras().getParcelable("no_gps_location") instanceof Location) { - network = network.getExtras().getParcelable("no_gps_location"); - } - return network; - } - return null; - } - - private boolean hasCoarseLocationPermission() { - return context.checkCallingPermission(ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED || hasFineLocationPermission(); - } - - private boolean hasFineLocationPermission() { - return context.checkCallingPermission(ACCESS_FINE_LOCATION) == PERMISSION_GRANTED; - } - - private boolean hasMockLocationPermission() { - return context.checkCallingPermission(ACCESS_MOCK_LOCATION) == PERMISSION_GRANTED; - } - - private void requestLocationUpdates(LocationRequestHelper request) { - LocationRequestHelper old = null; - for (LocationRequestHelper req : currentRequests) { - if (req.respondsTo(request.pendingIntent) || req.respondsTo(request.listener) || req.respondsTo(request.callback)) { - old = req; - break; - } - } - if (old != null) { - Log.d(TAG, "Removing replaced location request: " + old); - currentRequests.remove(old); - } - currentRequests.add(request); - if (gpsProvider != null && request.hasFinePermission() && request.locationRequest.getPriority() == PRIORITY_HIGH_ACCURACY) { - Log.d(TAG, "Registering request with high accuracy location provider"); - gpsProvider.addRequest(request); - } else if (gpsProvider != null && old != null) { - Log.d(TAG, "Unregistering request with high accuracy location provider"); - gpsProvider.removeRequest(old); - } else { - Log.w(TAG, "Not providing high accuracy location: missing permission"); - } - if (networkProvider != null && request.hasCoarsePermission() && request.locationRequest.getPriority() != PRIORITY_PASSIVE) { - Log.d(TAG, "Registering request with low accuracy location provider"); - networkProvider.addRequest(request); - } else if (networkProvider != null && old != null) { - Log.d(TAG, "Unregistering request with low accuracy location provider"); - networkProvider.removeRequest(old); - } else { - Log.w(TAG, "Not providing low accuracy location: missing permission"); - } - Location lastLocation = getLocation(request.hasFinePermission(), request.hasCoarsePermission()); - if (lastLocation != null && lastLocation.getTime() > System.currentTimeMillis() - request.locationRequest.getMaxUpdateAgeMillis()) { - Log.d(TAG, "Reporting previous location as it's newer than " + request.locationRequest.getMaxUpdateAgeMillis() + "ms"); - request.report(lastLocation); - } else { - Log.d(TAG, "Not reporting previous location as it's older than " + request.locationRequest.getMaxUpdateAgeMillis() + "ms"); - } - } - - public void requestLocationUpdates(LocationRequest request, ILocationListener listener, String packageName) { - requestLocationUpdates(new LocationRequestHelper(context, request, packageName, Binder.getCallingUid(), listener)); - } - - public void requestLocationUpdates(LocationRequest request, PendingIntent intent, String packageName) { - requestLocationUpdates(new LocationRequestHelper(context, request, packageName, Binder.getCallingUid(), intent)); - } - - private void removeLocationUpdates(LocationRequestHelper request) { - currentRequests.remove(request); - if (gpsProvider != null) gpsProvider.removeRequest(request); - if (networkProvider != null) networkProvider.removeRequest(request); - } - - public void removeLocationUpdates(ILocationListener listener, String packageName) { - for (int i = 0; i < currentRequests.size(); i++) { - if (currentRequests.get(i).respondsTo(listener)) { - removeLocationUpdates(currentRequests.get(i)); - i--; - } - } - } - - public void removeLocationUpdates(PendingIntent intent, String packageName) { - for (int i = 0; i < currentRequests.size(); i++) { - if (currentRequests.get(i).respondsTo(intent)) { - removeLocationUpdates(currentRequests.get(i)); - i--; - } - } - } - - public void updateLocationRequest(LocationRequestUpdateData data) { - try { - Log.d(TAG, "updateLocationRequest: " + data); - String packageName = PackageUtils.getCallingPackage(context); - if (data.pendingIntent != null) - packageName = PackageUtils.packageFromPendingIntent(data.pendingIntent); - Log.d(TAG, "Using source package: " + packageName); - if (data.opCode == LocationRequestUpdateData.REQUEST_UPDATES) { - requestLocationUpdates(new LocationRequestHelper(context, packageName, Binder.getCallingUid(), data)); - } else if (data.opCode == LocationRequestUpdateData.REMOVE_UPDATES) { - for (int i = 0; i < currentRequests.size(); i++) { - if (currentRequests.get(i).respondsTo(data.listener) - || currentRequests.get(i).respondsTo(data.pendingIntent) - || currentRequests.get(i).respondsTo(data.callback)) { - removeLocationUpdates(currentRequests.get(i)); - i--; - } - } - } - Log.d(TAG, "Updated current requests, verifying"); - verifyCurrentRequests(); - if (data.fusedLocationProviderCallback != null) { - try { - Log.d(TAG, "Send success result to " + packageName); - data.fusedLocationProviderCallback.onFusedLocationProviderResult(FusedLocationProviderResult.SUCCESS); - } catch (RemoteException ignored) { - } - } - } catch (Exception e) { - Log.w(TAG, "Exception in updateLocationRequest", e); - if (data.fusedLocationProviderCallback != null) { - try { - Log.d(TAG, "Send internal error result"); - data.fusedLocationProviderCallback.onFusedLocationProviderResult(FusedLocationProviderResult.create(Status.INTERNAL_ERROR)); - } catch (RemoteException ignored) { - } - } - } - } - - public void setMockMode(boolean mockMode) { - if (!hasMockLocationPermission()) - return; - mockProvider.setMockEnabled(mockMode); - } - - public void setMockLocation(Location mockLocation) { - if (!hasMockLocationPermission()) - return; - mockProvider.setLocation(mockLocation); - } - - private void verifyCurrentRequests() { - handler.removeCallbacks(verifyCurrentRequestsRunnable); - try { - for (int i = 0; i < currentRequests.size(); i++) { - LocationRequestHelper request = currentRequests.get(i); - if (!request.isActive()) { - removeLocationUpdates(request); - i--; - } - } - } catch (Exception e) { - Log.w(TAG, e); - } - handler.postDelayed(verifyCurrentRequestsRunnable, VERIFY_CURRENT_REQUESTS_INTERVAL_MS); - } - - @Override - public void onLocationChanged() { - for (int i = 0; i < currentRequests.size(); i++) { - LocationRequestHelper request = currentRequests.get(i); - if (!request.report(getLocation(request.initialHasFinePermission, request.initialHasCoarsePermission))) { - removeLocationUpdates(request); - i--; - } - } - } - - public void dump(PrintWriter writer) { - if (gpsProvider != null) gpsProvider.dump(writer); - if (networkProvider != null) networkProvider.dump(writer); - writer.println(currentRequests.size() + " requests:"); - for (LocationRequestHelper request : currentRequests) { - request.dump(writer); - } - } -} diff --git a/play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManagerService.java b/play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManagerService.java deleted file mode 100644 index 4629d00d61..0000000000 --- a/play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManagerService.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.location; - -import android.os.RemoteException; -import android.util.Log; - -import com.google.android.gms.common.Feature; -import com.google.android.gms.common.internal.ConnectionInfo; -import com.google.android.gms.common.internal.GetServiceRequest; -import com.google.android.gms.common.internal.IGmsCallbacks; - -import org.microg.gms.BaseService; -import org.microg.gms.common.GmsService; - -import java.io.FileDescriptor; -import java.io.PrintWriter; - -public class GoogleLocationManagerService extends BaseService { - private final GoogleLocationManagerServiceImpl impl = new GoogleLocationManagerServiceImpl(this, getLifecycle()); - - public GoogleLocationManagerService() { - super("LocationManager", GmsService.LOCATION_MANAGER, GmsService.GEODATA, GmsService.PLACE_DETECTION); - } - - @Override - public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException { - impl.invokeOnceReady(() -> { - try { - ConnectionInfo info = new ConnectionInfo(); - info.features = FEATURES; - callback.onPostInitCompleteWithConnectionInfo(0, impl.asBinder(), info); - } catch (RemoteException e) { - Log.w(TAG, e); - } - }); - } - - @Override - protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { - impl.getLocationManager().dump(writer); - } - - public static final Feature[] FEATURES = new Feature[] { - new Feature("get_current_location", 1), - new Feature("support_context_feature_id", 1), - new Feature("name_ulr_private", 1), - new Feature("driving_mode", 6), - new Feature("name_sleep_segment_request", 1) - }; -} diff --git a/play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManagerServiceImpl.java b/play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManagerServiceImpl.java deleted file mode 100644 index bb21811f84..0000000000 --- a/play-services-location/core/src/main/java/org/microg/gms/location/GoogleLocationManagerServiceImpl.java +++ /dev/null @@ -1,436 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.location; - -import android.app.PendingIntent; -import android.content.Context; -import android.location.Location; -import android.os.Binder; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Parcel; -import android.os.RemoteException; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleOwner; - -import com.google.android.gms.common.api.Status; -import com.google.android.gms.common.api.internal.IStatusCallback; -import com.google.android.gms.location.ActivityRecognitionResult; -import com.google.android.gms.location.GeofencingRequest; -import com.google.android.gms.location.GestureRequest; -import com.google.android.gms.location.ILocationListener; -import com.google.android.gms.location.LocationAvailability; -import com.google.android.gms.location.LocationRequest; -import com.google.android.gms.location.LocationSettingsRequest; -import com.google.android.gms.location.LocationSettingsResult; -import com.google.android.gms.location.LocationSettingsStates; -import com.google.android.gms.location.internal.DeviceOrientationRequestUpdateData; -import com.google.android.gms.location.internal.IFusedLocationProviderCallback; -import com.google.android.gms.location.internal.IGeofencerCallbacks; -import com.google.android.gms.location.internal.IGoogleLocationManagerService; -import com.google.android.gms.location.internal.ISettingsCallbacks; -import com.google.android.gms.location.internal.LocationRequestInternal; -import com.google.android.gms.location.internal.LocationRequestUpdateData; -import com.google.android.gms.location.internal.ParcelableGeofence; -//import com.google.android.gms.location.places.AutocompleteFilter; -//import com.google.android.gms.location.places.NearbyAlertRequest; -//import com.google.android.gms.location.places.PlaceFilter; -//import com.google.android.gms.location.places.PlaceReport; -//import com.google.android.gms.location.places.PlaceRequest; -//import com.google.android.gms.location.places.UserAddedPlace; -//import com.google.android.gms.location.places.UserDataType; -//import com.google.android.gms.location.places.internal.IPlacesCallbacks; -//import com.google.android.gms.location.places.internal.PlacesParams; -//import com.google.android.gms.maps.model.LatLng; -//import com.google.android.gms.maps.model.LatLngBounds; - -import org.microg.gms.common.PackageUtils; - -import java.util.Arrays; -import java.util.List; - -public class GoogleLocationManagerServiceImpl extends IGoogleLocationManagerService.Stub implements LifecycleOwner { - private static final String TAG = "GmsLocManagerSvcImpl"; - - private final Context context; - private final Lifecycle lifecycle; - private GoogleLocationManager locationManager; - - public GoogleLocationManagerServiceImpl(Context context, Lifecycle lifecycle) { - this.context = context; - this.lifecycle = lifecycle; - } - - @NonNull - @Override - public Lifecycle getLifecycle() { - return lifecycle; - } - - public void invokeOnceReady(Runnable runnable) { - getLocationManager().invokeOnceReady(runnable); - } - - public synchronized GoogleLocationManager getLocationManager() { - if (locationManager == null) - locationManager = new GoogleLocationManager(context, lifecycle); - return locationManager; - } - - @Override - public void addGeofencesList(List geofences, PendingIntent pendingIntent, - IGeofencerCallbacks callbacks, String packageName) throws RemoteException { - Log.d(TAG, "addGeofencesList: " + geofences); - PackageUtils.checkPackageUid(context, packageName, Binder.getCallingUid()); - } - - @Override - public void removeGeofencesByIntent(PendingIntent pendingIntent, IGeofencerCallbacks callbacks, - String packageName) throws RemoteException { - Log.d(TAG, "removeGeofencesByIntent: " + pendingIntent); - PackageUtils.checkPackageUid(context, packageName, Binder.getCallingUid()); - } - - @Override - public void removeGeofencesById(String[] geofenceRequestIds, IGeofencerCallbacks callbacks, - String packageName) throws RemoteException { - Log.d(TAG, "removeGeofencesById: " + Arrays.toString(geofenceRequestIds)); - PackageUtils.checkPackageUid(context, packageName, Binder.getCallingUid()); - } - - @Override - public void removeAllGeofences(IGeofencerCallbacks callbacks, String packageName) throws RemoteException { - Log.d(TAG, "removeAllGeofences"); - PackageUtils.checkPackageUid(context, packageName, Binder.getCallingUid()); - } - - @Override - public void requestActivityUpdates(long detectionIntervalMillis, boolean alwaysTrue, - PendingIntent callbackIntent) throws RemoteException { - Log.d(TAG, "requestActivityUpdates: " + callbackIntent); - } - - @Override - public void removeActivityUpdates(PendingIntent callbackIntent) throws RemoteException { - Log.d(TAG, "removeActivityUpdates: " + callbackIntent); - } - - @Override - public ActivityRecognitionResult getLastActivity(String packageName) throws RemoteException { - Log.d(TAG, "getLastActivity"); - PackageUtils.checkPackageUid(context, packageName, Binder.getCallingUid()); - return null; - } - - @Override - public Status requestGestureUpdates(GestureRequest request, PendingIntent pendingIntent) throws RemoteException { - Log.d(TAG, "requestGestureUpdates"); - return null; - } - - @Override - public Status iglms61(PendingIntent pendingIntent) throws RemoteException { - Log.d(TAG, "iglms61"); - return null; - } - - @Override - public Location getLastLocation() throws RemoteException { - Log.d(TAG, "getLastLocation"); - return getLocationManager().getLastLocation(PackageUtils.getCallingPackage(context)); - } - - @Override - public void flushLocations(IFusedLocationProviderCallback callback) throws RemoteException { - Log.d(TAG, "flushLocations"); - //getLocationManager().flushLocations(); - } - - @Override - public void requestLocationUpdatesWithListener(LocationRequest request, - final ILocationListener listener) throws RemoteException { - Log.d(TAG, "requestLocationUpdatesWithListener: " + request); - getLocationManager().requestLocationUpdates(request, listener, PackageUtils.getCallingPackage(context)); - } - - @Override - public void requestLocationUpdatesWithIntent(LocationRequest request, - PendingIntent callbackIntent) throws RemoteException { - Log.d(TAG, "requestLocationUpdatesWithIntent: " + request); - getLocationManager().requestLocationUpdates(request, callbackIntent, PackageUtils.packageFromPendingIntent(callbackIntent)); - } - - @Override - public void removeLocationUpdatesWithListener(ILocationListener listener) - throws RemoteException { - Log.d(TAG, "removeLocationUpdatesWithListener: " + listener); - getLocationManager().removeLocationUpdates(listener, PackageUtils.getCallingPackage(context)); - } - - @Override - public void removeLocationUpdatesWithIntent(PendingIntent callbackIntent) - throws RemoteException { - Log.d(TAG, "removeLocationUpdatesWithIntent: " + callbackIntent); - getLocationManager().removeLocationUpdates(callbackIntent, PackageUtils.packageFromPendingIntent(callbackIntent)); - } - - @Override - public void updateLocationRequest(LocationRequestUpdateData locationRequestUpdateData) throws RemoteException { - Log.d(TAG, "updateLocationRequest: " + locationRequestUpdateData); - getLocationManager().updateLocationRequest(locationRequestUpdateData); - } - - @Override - public void setMockMode(boolean mockMode) throws RemoteException { - Log.d(TAG, "setMockMode: " + mockMode); - getLocationManager().setMockMode(mockMode); - } - - @Override - public void setMockLocation(Location mockLocation) throws RemoteException { - Log.d(TAG, "setMockLocation: " + mockLocation); - getLocationManager().setMockLocation(mockLocation); - } - - @Override - public void injectLocation(Location mockLocation, int injectionType) throws RemoteException { - Log.d(TAG, "injectLocation[" + injectionType + "]: " + mockLocation); - } - -// @Override -// public void iglms14(LatLngBounds var1, int var2, PlaceFilter var3, PlacesParams var4, -// IPlacesCallbacks var5) throws RemoteException { -// Log.d(TAG, "iglms14: " + var1); -// } -// -// @Override -// public void iglms15(String var1, PlacesParams var2, IPlacesCallbacks var3) -// throws RemoteException { -// Log.d(TAG, "iglms15: " + var1); -// } -// -// @Override -// public void iglms16(LatLng var1, PlaceFilter var2, PlacesParams var3, IPlacesCallbacks var4) -// throws RemoteException { -// Log.d(TAG, "iglms16: " + var1); -// } -// -// @Override -// public void iglms17(PlaceFilter var1, PlacesParams var2, IPlacesCallbacks var3) -// throws RemoteException { -// Log.d(TAG, "iglms17: " + var1); -// } -// -// @Override -// public void iglms18(PlaceRequest var1, PlacesParams var2, PendingIntent var3) -// throws RemoteException { -// Log.d(TAG, "iglms18: " + var1); -// } -// -// @Override -// public void iglms19(PlacesParams var1, PendingIntent var2) throws RemoteException { -// Log.d(TAG, "iglms19: " + var1); -// } - - @Override - public void requestLocationUpdatesWithPackage(LocationRequest request, ILocationListener listener, - String packageName) throws RemoteException { - Log.d(TAG, "requestLocationUpdatesWithPackage: " + request); - PackageUtils.checkPackageUid(context, packageName, Binder.getCallingUid()); - getLocationManager().requestLocationUpdates(request, listener, packageName); - } - - @Override - public Location getLastLocationWithPackage(String packageName) throws RemoteException { - Log.d(TAG, "getLastLocationWithPackage: " + packageName); - PackageUtils.checkPackageUid(context, packageName, Binder.getCallingUid()); - return getLocationManager().getLastLocation(packageName); - } - - @Override - public Location getLastLocationWith(String s) throws RemoteException { - Log.d(TAG, "getLastLocationWith: " + s); - return getLastLocation(); - } - -// @Override -// public void iglms25(PlaceReport var1, PlacesParams var2) throws RemoteException { -// Log.d(TAG, "iglms25: " + var1); -// } - - @Override - public LocationAvailability getLocationAvailabilityWithPackage(String packageName) throws RemoteException { - Log.d(TAG, "getLocationAvailabilityWithPackage: " + packageName); - PackageUtils.checkPackageUid(context, packageName, Binder.getCallingUid()); - return new LocationAvailability(); - } - - @Override - public void removeSleepSegmentUpdates(PendingIntent pendingIntent, IStatusCallback callback) throws RemoteException { - Log.d(TAG, "removeSleepSegmentUpdates"); - } - -// @Override -// public void iglms42(String var1, PlacesParams var2, IPlacesCallbacks var3) -// throws RemoteException { -// Log.d(TAG, "iglms42: " + var1); -// } -// -// @Override -// public void iglms46(UserAddedPlace var1, PlacesParams var2, IPlacesCallbacks var3) -// throws RemoteException { -// Log.d(TAG, "iglms46: " + var1); -// } -// -// @Override -// public void iglms47(LatLngBounds var1, int var2, String var3, PlaceFilter var4, -// PlacesParams var5, IPlacesCallbacks var6) throws RemoteException { -// Log.d(TAG, "iglms47: " + var1); -// } -// -// @Override -// public void iglms48(NearbyAlertRequest var1, PlacesParams var2, PendingIntent var3) -// throws RemoteException { -// Log.d(TAG, "iglms48: " + var1); -// } -// -// @Override -// public void iglms49(PlacesParams var1, PendingIntent var2) throws RemoteException { -// Log.d(TAG, "iglms49: " + var1); -// } -// -// @Override -// public void iglms50(UserDataType var1, LatLngBounds var2, List var3, PlacesParams var4, -// IPlacesCallbacks var5) throws RemoteException { -// Log.d(TAG, "iglms50: " + var1); -// } - - @Override - public IBinder iglms51() throws RemoteException { - Log.d(TAG, "iglms51"); - return null; - } - - @Override - public void requestLocationSettingsDialog(LocationSettingsRequest settingsRequest, ISettingsCallbacks callback, String packageName) throws RemoteException { - Log.d(TAG, "requestLocationSettingsDialog: " + settingsRequest); - PackageUtils.getAndCheckCallingPackage(context, packageName); - (new Handler(Looper.getMainLooper())).post(() -> { - try { - callback.onLocationSettingsResult(new LocationSettingsResult(new LocationSettingsStates(true, true, true, true, true, true), Status.SUCCESS)); - } catch (RemoteException e) { - Log.w(TAG, e); - } - }); - } - - @Override - public void removeActivityTransitionUpdates(PendingIntent pendingIntent, IStatusCallback callback) throws RemoteException { - Log.d(TAG, "removeActivityTransitionUpdates"); - } - - @Override - public void updateDeviceOrientationRequest(DeviceOrientationRequestUpdateData request) throws RemoteException { - Log.d(TAG, "updateDeviceOrientationRequest: " + request); - } - - @Override - public boolean setActivityRecognitionMode(int mode) throws RemoteException { - Log.d(TAG, "setActivityRecognitionMode: " + mode); - return false; - } - - @Override - public void requestLocationUpdatesInternalWithListener(LocationRequestInternal request, - ILocationListener listener) throws RemoteException { - Log.d(TAG, "requestLocationUpdatesInternalWithListener: " + request); - getLocationManager().requestLocationUpdates(request.getRequest(), listener, PackageUtils.getCallingPackage(context)); - } - - @Override - public void requestLocationUpdatesInternalWithIntent(LocationRequestInternal request, - PendingIntent callbackIntent) throws RemoteException { - Log.d(TAG, "requestLocationUpdatesInternalWithIntent: " + request); - getLocationManager().requestLocationUpdates(request.getRequest(), callbackIntent, PackageUtils.packageFromPendingIntent(callbackIntent)); - } - - @Override - public IBinder iglms54() throws RemoteException { - Log.d(TAG, "iglms54"); - return null; - } - -// @Override -// public void iglms55(String var1, LatLngBounds var2, AutocompleteFilter var3, PlacesParams var4, -// IPlacesCallbacks var5) throws RemoteException { -// Log.d(TAG, "iglms55: " + var1); -// } - - @Override - public void addGeofences(GeofencingRequest geofencingRequest, PendingIntent pendingIntent, - IGeofencerCallbacks callbacks) throws RemoteException { - Log.d(TAG, "addGeofences: " + geofencingRequest); - } - -// @Override -// public void iglms58(List var1, PlacesParams var2, IPlacesCallbacks var3) -// throws RemoteException { -// Log.d(TAG, "iglms58: " + var1); -// } - - @Override - public void iglms65(PendingIntent pendingIntent, IStatusCallback callback) throws RemoteException { - Log.d(TAG, "iglms65"); - } - - @Override - public void iglms66(PendingIntent pendingIntent, IStatusCallback callback) throws RemoteException { - Log.d(TAG, "iglms66"); - } - - @Override - public void iglms68(PendingIntent pendingIntent, IStatusCallback callback) throws RemoteException { - Log.d(TAG, "iglms68"); - } - - @Override - public void iglms71(IStatusCallback callback) throws RemoteException { - Log.d(TAG, "iglms71"); - } - - @Override - public void iglms76(PendingIntent pendingIntent) throws RemoteException { - Log.d(TAG, "iglms76"); - } - - @Override - public int iglms78() throws RemoteException { - Log.d(TAG, "iglms78"); - return 0; - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - if (super.onTransact(code, data, reply, flags)) return true; - Log.d(TAG, "onTransact [unknown]: " + code + ", " + data + ", " + flags); - return false; - } -} diff --git a/play-services-location/core/src/main/java/org/microg/gms/location/LocationChangeListener.java b/play-services-location/core/src/main/java/org/microg/gms/location/LocationChangeListener.java deleted file mode 100644 index f574746d6f..0000000000 --- a/play-services-location/core/src/main/java/org/microg/gms/location/LocationChangeListener.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.location; - -public interface LocationChangeListener { - public void onLocationChanged(); -} diff --git a/play-services-location/core/src/main/java/org/microg/gms/location/LocationRequestHelper.java b/play-services-location/core/src/main/java/org/microg/gms/location/LocationRequestHelper.java deleted file mode 100644 index 52119defc5..0000000000 --- a/play-services-location/core/src/main/java/org/microg/gms/location/LocationRequestHelper.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.location; - -import android.annotation.TargetApi; -import android.app.AppOpsManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.location.Location; -import android.os.Binder; -import android.os.Build; -import android.os.RemoteException; -import android.os.SystemClock; -import android.util.Log; - -import com.google.android.gms.location.ILocationCallback; -import com.google.android.gms.location.ILocationListener; -import com.google.android.gms.location.LocationRequest; -import com.google.android.gms.location.LocationResult; -import com.google.android.gms.location.internal.LocationRequestUpdateData; - -import java.io.PrintWriter; -import java.util.Arrays; -import java.util.UUID; - -import static android.Manifest.permission.ACCESS_COARSE_LOCATION; -import static android.Manifest.permission.ACCESS_FINE_LOCATION; - -public class LocationRequestHelper { - public static final String TAG = "GmsLocRequestHelper"; - private final Context context; - public final LocationRequest locationRequest; - public final boolean initialHasFinePermission; - public final boolean initialHasCoarsePermission; - public final String packageName; - public final int uid; - private final boolean selfHasAppOpsRights; - public ILocationListener listener; - public PendingIntent pendingIntent; - public ILocationCallback callback; - public String id = UUID.randomUUID().toString(); - public final long start; - - private Location lastReport; - private int numReports = 0; - - private LocationRequestHelper(Context context, LocationRequest locationRequest, String packageName, int uid) { - this.context = context; - this.locationRequest = locationRequest; - this.packageName = packageName; - this.uid = uid; - this.start = SystemClock.elapsedRealtime(); - - this.initialHasFinePermission = context.getPackageManager().checkPermission(ACCESS_FINE_LOCATION, packageName) == PackageManager.PERMISSION_GRANTED; - this.initialHasCoarsePermission = context.getPackageManager().checkPermission(ACCESS_COARSE_LOCATION, packageName) == PackageManager.PERMISSION_GRANTED; - - this.selfHasAppOpsRights = context.getPackageManager().checkPermission("android.permission.UPDATE_APP_OPS_STATS", context.getPackageName()) == PackageManager.PERMISSION_GRANTED; - } - - public LocationRequestHelper(Context context, LocationRequest locationRequest, String packageName, int uid, ILocationListener listener) { - this(context, locationRequest, packageName, uid); - this.listener = listener; - } - - public LocationRequestHelper(Context context, LocationRequest locationRequest, String packageName, int uid, PendingIntent pendingIntent) { - this(context, locationRequest, packageName, uid); - this.pendingIntent = pendingIntent; - } - - public LocationRequestHelper(Context context, String packageName, int uid, LocationRequestUpdateData data) { - this(context, data.request.getRequest(), packageName, uid); - this.listener = data.listener; - this.pendingIntent = data.pendingIntent; - this.callback = data.callback; - } - - public boolean isActive() { - if (!hasCoarsePermission()) return false; - if (locationRequest.getExpirationTime() < SystemClock.elapsedRealtime() || locationRequest.getDurationMillis() < (SystemClock.elapsedRealtime() - start)) return false; - if (listener != null) { - try { - return listener.asBinder().isBinderAlive(); - } catch (Exception e) { - return false; - } - } else if (pendingIntent != null) { - return true; - } else if (callback != null) { - try { - return callback.asBinder().isBinderAlive(); - } catch (Exception e) { - return false; - } - } else { - return false; - } - } - - public boolean locationIsValid(Location location) { - if (location == null) return false; - if (Double.isNaN(location.getLatitude()) || location.getLatitude() > 90 || location.getLatitude() < -90) return false; - if (Double.isNaN(location.getLongitude()) || location.getLongitude() > 180 || location.getLongitude() < -180) return false; - return true; - } - - /** - * @return whether to continue sending reports to this {@link LocationRequestHelper} - */ - public boolean report(Location location) { - if (!isActive()) return false; - if (!locationIsValid(location)) return true; - if (lastReport != null) { - if (location.equals(lastReport)) { - return true; - } - if (location.getTime() - lastReport.getTime() < locationRequest.getMinUpdateIntervalMillis()) { - return true; - } - if (location.distanceTo(lastReport) < locationRequest.getMinUpdateDistanceMeters()) { - return true; - } - } - lastReport = new Location(location); - lastReport.setProvider("fused"); - Log.d(TAG, "sending Location: " + location + " to " + packageName); - if (listener != null) { - try { - listener.onLocationChanged(lastReport); - } catch (RemoteException e) { - return false; - } - } else if (pendingIntent != null) { - Intent intent = new Intent(); - intent.putExtra("com.google.android.location.LOCATION", lastReport); - try { - pendingIntent.send(context, 0, intent); - } catch (PendingIntent.CanceledException e) { - return false; - } - } else if (callback != null) { - try { - callback.onLocationResult(LocationResult.create(Arrays.asList(lastReport))); - } catch (RemoteException e) { - return false; - } - } else { - return false; - } - numReports++; - return numReports < locationRequest.getMaxUpdates(); - } - - @Override - public String toString() { - return "LocationRequestHelper{" + - "locationRequest=" + locationRequest + - ", hasFinePermission=" + hasFinePermission() + - ", hasCoarsePermission=" + hasCoarsePermission() + - ", packageName='" + packageName + '\'' + - ", lastReport=" + lastReport + - '}'; - } - - public boolean respondsTo(ILocationListener listener) { - return this.listener != null && listener != null && - this.listener.asBinder().equals(listener.asBinder()); - } - - public boolean respondsTo(ILocationCallback callback) { - return this.callback != null && callback != null && - this.callback.asBinder().equals(callback.asBinder()); - } - - public boolean respondsTo(PendingIntent pendingIntent) { - return this.pendingIntent != null && this.pendingIntent.equals(pendingIntent); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - LocationRequestHelper that = (LocationRequestHelper) o; - - if (!locationRequest.equals(that.locationRequest)) return false; - if (packageName != null ? !packageName.equals(that.packageName) : that.packageName != null) return false; - if (listener != null ? !listener.equals(that.listener) : that.listener != null) return false; - if (pendingIntent != null ? !pendingIntent.equals(that.pendingIntent) : that.pendingIntent != null) - return false; - return !(callback != null ? !callback.equals(that.callback) : that.callback != null); - } - - public boolean hasFinePermission() { - if (Build.VERSION.SDK_INT >= 19) { - return isAppOpsAllowed(AppOpsManager.OPSTR_FINE_LOCATION, initialHasFinePermission); - } else { - return initialHasFinePermission; - } - } - - public boolean hasCoarsePermission() { - if (Build.VERSION.SDK_INT >= 19) { - return isAppOpsAllowed(AppOpsManager.OPSTR_COARSE_LOCATION, initialHasCoarsePermission); - } else { - return initialHasCoarsePermission; - } - } - - @TargetApi(19) - private boolean isAppOpsAllowed(String op, boolean def) { - AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - if (appOpsManager == null) return def; - try { - if (Binder.getCallingUid() == uid && Build.VERSION.SDK_INT >= 23) { - return appOpsManager.noteProxyOpNoThrow(op, packageName) == AppOpsManager.MODE_ALLOWED; - } else if (Build.VERSION.SDK_INT >= 29) { - return appOpsManager.noteProxyOpNoThrow(op, packageName, uid) == AppOpsManager.MODE_ALLOWED; - } else if (selfHasAppOpsRights) { - return appOpsManager.noteOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED; - } else { - // TODO: More variant that works pre-29 and without perms? - Log.w(TAG, "Can't check appops (yet)"); - return def; - } - } catch (Exception e) { - Log.w(TAG, e); - return def; - } - } - - @Override - public int hashCode() { - int result = locationRequest.hashCode(); - result = 31 * result + (packageName != null ? packageName.hashCode() : 0); - result = 31 * result + (listener != null ? listener.hashCode() : 0); - result = 31 * result + (pendingIntent != null ? pendingIntent.hashCode() : 0); - result = 31 * result + (callback != null ? callback.hashCode() : 0); - return result; - } - - public void dump(PrintWriter writer) { - String type = "unknown"; - if (listener != null) type = "listener"; - if (pendingIntent != null) type = "pending intent"; - if (callback != null) type = "callback"; - writer.println(" " + id + " package=" + packageName + " type=" + type); - writer.println(" request: " + locationRequest); - writer.println(" last location: " + lastReport); - } -} diff --git a/play-services-location/core/src/main/java/org/microg/gms/location/MockLocationProvider.java b/play-services-location/core/src/main/java/org/microg/gms/location/MockLocationProvider.java deleted file mode 100644 index 75ebc6a8f8..0000000000 --- a/play-services-location/core/src/main/java/org/microg/gms/location/MockLocationProvider.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.location; - -import android.location.Location; -import android.os.Bundle; - -import static com.google.android.gms.location.FusedLocationProviderClient.KEY_MOCK_LOCATION; - -public class MockLocationProvider { - private boolean mockEnabled = false; - private Location mockLocation = null; - private final LocationChangeListener changeListener; - - public MockLocationProvider(LocationChangeListener changeListener) { - this.changeListener = changeListener; - } - - public void setMockEnabled(boolean mockEnabled) { - this.mockEnabled = mockEnabled; - } - - public Location getLocation() { - return mockEnabled ? mockLocation : null; - } - - public void setLocation(Location mockLocation) { - if (mockLocation.getExtras() == null) { - mockLocation.setExtras(new Bundle()); - } - mockLocation.getExtras().putBoolean(KEY_MOCK_LOCATION, false); - this.mockLocation = mockLocation; - this.changeListener.onLocationChanged(); - } -} diff --git a/play-services-location/core/src/main/java/org/microg/gms/location/RealLocationProvider.java b/play-services-location/core/src/main/java/org/microg/gms/location/RealLocationProvider.java deleted file mode 100644 index d66c3a62c8..0000000000 --- a/play-services-location/core/src/main/java/org/microg/gms/location/RealLocationProvider.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.location; - -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.Bundle; -import android.os.Looper; -import android.util.Log; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -@SuppressWarnings("MissingPermission") -public class RealLocationProvider { - public static final String TAG = "GmsLocProviderReal"; - private static final int MIN_GPS_TIME = 60000; - - private final LocationManager locationManager; - private final String name; - private final AtomicBoolean connected = new AtomicBoolean(false); - private final LocationChangeListener changeListener; - - private long connectedMinTime; - private float connectedMinDistance; - private Location lastLocation; - private final List requests = new ArrayList(); - private LocationListener listener = new LocationListener() { - @Override - public void onLocationChanged(Location location) { - lastLocation = new Location(location); - try { - lastLocation.getExtras().keySet(); // call to unparcel() - } catch (Exception e) { - // Sometimes we need to define the correct ClassLoader before unparcel(). Ignore those. - } - changeListener.onLocationChanged(); - } - - @Override - public void onStatusChanged(String s, int i, Bundle bundle) { - - } - - @Override - public void onProviderEnabled(String s) { - - } - - @Override - public void onProviderDisabled(String s) { - - } - }; - - public RealLocationProvider(LocationManager locationManager, String name, LocationChangeListener changeListener) { - this.locationManager = locationManager; - this.name = name; - this.changeListener = changeListener; - updateLastLocation(); - } - - private void updateLastLocation() { - Location newLocation = locationManager.getLastKnownLocation(name); - if (newLocation != null) lastLocation = newLocation; - } - - public void invokeOnceReady(Runnable runnable) { - // Always ready - runnable.run(); - } - - public Location getLastLocation() { - if (!connected.get()) { - updateLastLocation(); - } - if (lastLocation == null) { - Log.d(TAG, "uh-ok: last location for " + name + " is null!"); - } - return lastLocation; - } - - public void addRequest(LocationRequestHelper request) { - Log.d(TAG, name + ": addRequest " + request); - for (int i = 0; i < requests.size(); i++) { - LocationRequestHelper req = requests.get(i); - if (req.respondsTo(request.pendingIntent) || req.respondsTo(request.listener) || req.respondsTo(request.callback)) { - requests.remove(i); - i--; - } - } - requests.add(request); - updateConnection(); - } - - public void removeRequest(LocationRequestHelper request) { - Log.d(TAG, name + ": removeRequest " + request); - requests.remove(request); - updateConnection(); - } - - private synchronized void updateConnection() { - if (connected.get() && requests.isEmpty()) { - Log.d(TAG, name + ": no longer requesting location update"); - locationManager.removeUpdates(listener); - connected.set(false); - } else if (!requests.isEmpty()) { - long minTime = Long.MAX_VALUE; - float minDistance = Float.MAX_VALUE; - StringBuilder sb = new StringBuilder(); - for (LocationRequestHelper request : requests) { - minTime = Math.min(request.locationRequest.getIntervalMillis(), minTime); - minDistance = Math.min(request.locationRequest.getMinUpdateDistanceMeters(), minDistance); - if (sb.length() != 0) sb.append(", "); - sb.append(request.packageName).append(":").append(request.locationRequest.getIntervalMillis()).append("ms"); - } - if (minTime > MIN_GPS_TIME && name.equals("gps")) { - Log.d(TAG, name + ": ignoring request as " + minTime + "ms (" + sb + "), is less than " + MIN_GPS_TIME); - locationManager.removeUpdates(listener); - connected.set(false); - return; - } - Log.d(TAG, name + ": requesting location updates with interval " + minTime + "ms (" + sb + "), minDistance=" + minDistance); - if (connected.get()) { - if (connectedMinTime != minTime || connectedMinDistance != minDistance) { - locationManager.removeUpdates(listener); - locationManager.requestLocationUpdates(name, minTime, minDistance, listener, - Looper.getMainLooper()); - } - } else { - locationManager.requestLocationUpdates(name, minTime, minDistance, listener, Looper.getMainLooper()); - } - connected.set(true); - connectedMinTime = minTime; - connectedMinDistance = minDistance; - } - } - - public void dump(PrintWriter writer) { - if (writer != null) { - writer.println(name + " provider:"); - writer.println(" last location: " + lastLocation); - writer.println(" active: " + connected.get()); - if (connected.get()) { - writer.println(" interval: " + connectedMinTime); - writer.println(" distance: " + connectedMinDistance); - } - } - } -} diff --git a/play-services-location/core/src/main/java/org/microg/gms/location/ReportingAndroidService.java b/play-services-location/core/src/main/java/org/microg/gms/location/ReportingAndroidService.java deleted file mode 100644 index dd32d9e24d..0000000000 --- a/play-services-location/core/src/main/java/org/microg/gms/location/ReportingAndroidService.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.location; - -import android.os.RemoteException; - -import com.google.android.gms.common.internal.ConnectionInfo; -import com.google.android.gms.common.internal.GetServiceRequest; -import com.google.android.gms.common.internal.IGmsCallbacks; - -import org.microg.gms.BaseService; -import org.microg.gms.common.GmsService; - -public class ReportingAndroidService extends BaseService { - private ReportingServiceImpl reportingService = new ReportingServiceImpl(this); - - public ReportingAndroidService() { - super("GmsLocReportingSvc", GmsService.LOCATION_REPORTING); - } - - @Override - public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException { - ConnectionInfo info = new ConnectionInfo(); - info.features = GoogleLocationManagerService.FEATURES; - callback.onPostInitCompleteWithConnectionInfo(0, reportingService.asBinder(), info); - } -} diff --git a/play-services-location/core/src/main/java/org/microg/gms/location/ReportingServiceImpl.java b/play-services-location/core/src/main/java/org/microg/gms/location/ReportingServiceImpl.java deleted file mode 100644 index 5fedc85d81..0000000000 --- a/play-services-location/core/src/main/java/org/microg/gms/location/ReportingServiceImpl.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.location; - -import android.accounts.Account; -import android.content.Context; -import android.os.Parcel; -import android.os.RemoteException; -import android.util.Log; - -//import com.google.android.gms.location.places.PlaceReport; -import com.google.android.gms.location.reporting.OptInRequest; -import com.google.android.gms.location.reporting.ReportingState; -import com.google.android.gms.location.reporting.SendDataRequest; -import com.google.android.gms.location.reporting.UlrPrivateModeRequest; -import com.google.android.gms.location.reporting.UploadRequest; -import com.google.android.gms.location.reporting.UploadRequestResult; -import com.google.android.gms.location.reporting.internal.IReportingService; - -import org.microg.gms.common.PackageUtils; - -public class ReportingServiceImpl extends IReportingService.Stub { - private static final String TAG = "GmsLocReportSvcImpl"; - private Context context; - - public ReportingServiceImpl(Context context) { - this.context = context; - } - - @Override - public ReportingState getReportingState(Account account) throws RemoteException { - Log.d(TAG, "getReportingState"); - ReportingState state = new ReportingState(); - if (PackageUtils.callerHasExtendedAccess(context)) { - state.deviceTag = 0; - } - return state; - } - - @Override - public int tryOptInAccount(Account account) throws RemoteException { - OptInRequest request = new OptInRequest(); - request.account = account; - return tryOptIn(request); - } - - @Override - public UploadRequestResult requestUpload(UploadRequest request) throws RemoteException { - Log.d(TAG, "requestUpload"); - return new UploadRequestResult(); - } - - @Override - public int cancelUploadRequest(long l) throws RemoteException { - Log.d(TAG, "cancelUploadRequest"); - return 0; - } - -// @Override -// public int reportDeviceAtPlace(Account account, PlaceReport report) throws RemoteException { -// Log.d(TAG, "reportDeviceAtPlace"); -// return 0; -// } - - @Override - public int tryOptIn(OptInRequest request) throws RemoteException { - return 0; - } - - @Override - public int sendData(SendDataRequest request) throws RemoteException { - return 0; - } - - @Override - public int requestPrivateMode(UlrPrivateModeRequest request) throws RemoteException { - return 0; - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - if (super.onTransact(code, data, reply, flags)) { - return true; - } - - Log.d(TAG, "onTransact [unknown]: " + code + ", " + data + ", " + flags); - return false; - } -} diff --git a/play-services-location/core/src/main/java/org/microg/gms/location/UnifiedLocationProvider.kt b/play-services-location/core/src/main/java/org/microg/gms/location/UnifiedLocationProvider.kt deleted file mode 100644 index ad7c39e0d2..0000000000 --- a/play-services-location/core/src/main/java/org/microg/gms/location/UnifiedLocationProvider.kt +++ /dev/null @@ -1,141 +0,0 @@ -package org.microg.gms.location - -import android.content.Context -import android.location.Location -import android.os.Bundle -import android.util.Log -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.microg.nlp.client.LocationClient -import org.microg.nlp.service.api.Constants -import org.microg.nlp.service.api.ILocationListener -import org.microg.nlp.service.api.LocationRequest -import java.io.PrintWriter -import java.lang.Exception -import java.util.* -import kotlin.collections.ArrayList - -class UnifiedLocationProvider(private val context: Context, private val changeListener: LocationChangeListener, private val lifecycle: Lifecycle): LifecycleOwner { - private val client: LocationClient = LocationClient(context, lifecycle) - private var lastLocation: Location? = null - private val requests: MutableList = ArrayList() - private val activeRequestIds = hashSetOf() - private val activeRequestMutex = Mutex(false) - private val listener: ILocationListener = object : ILocationListener.Stub() { - override fun onLocation(statusCode: Int, location: Location?) { - if (statusCode == Constants.STATUS_OK && location != null) { - lastLocation = Location(location) - try { - for (key in lastLocation?.extras?.keySet()?.toList().orEmpty()) { - if (key?.startsWith("org.microg.nlp.") == true) { - lastLocation?.extras?.remove(key) - } - } - } catch (e:Exception){ - // Sometimes we need to define the correct ClassLoader before unparcel(). Ignore those. - } - changeListener.onLocationChanged() - } - } - } - private var ready = false - private val invokeOnceReady = hashSetOf() - - init { - updateLastLocation() - } - - private fun updateLastLocation() { - lifecycleScope.launchWhenStarted { - Log.d(TAG, "unified network: requesting last location") - val lastLocation = client.getLastLocation() - Log.d(TAG, "unified network: got last location: $lastLocation") - if (lastLocation != null) { - this@UnifiedLocationProvider.lastLocation = lastLocation - } - synchronized(invokeOnceReady) { - for (runnable in invokeOnceReady) { - runnable.run() - } - ready = true - } - } - } - - fun invokeOnceReady(runnable: Runnable) { - synchronized(invokeOnceReady) { - if (ready) runnable.run() - else invokeOnceReady.add(runnable) - } - } - - fun addRequest(request: LocationRequestHelper) { - Log.d(TAG, "unified network: addRequest $request") - for (i in 0..requests.size) { - if (i >= requests.size) break - val req = requests[i] - if (req.respondsTo(request.pendingIntent) || req.respondsTo(request.listener) || req.respondsTo(request.callback)) { - requests.removeAt(i) - } - } - requests.add(request) - lifecycleScope.launchWhenStarted { - updateConnection() - } - } - - fun removeRequest(request: LocationRequestHelper) { - Log.d(TAG, "unified network: removeRequest $request") - requests.remove(request) - - lifecycleScope.launchWhenStarted { - updateConnection() - } - } - - fun getLastLocation(): Location? { - if (lastLocation == null) { - Log.d(TAG, "uh-ok: last location for unified network is null!") - } - return lastLocation - } - - private suspend fun updateConnection() { - activeRequestMutex.withLock { - if (activeRequestIds.isNotEmpty() && requests.isEmpty()) { - Log.d(TAG, "unified network: no longer requesting location update") - for (id in activeRequestIds) { - client.cancelLocationRequestById(id) - } - activeRequestIds.clear() - } else if (requests.isNotEmpty()) { - val requests = ArrayList(requests).filter { it.isActive } - for (id in activeRequestIds.filter { id -> requests.none { it.id == id } }) { - client.cancelLocationRequestById(id) - } - for (request in requests.filter { it.id !in activeRequestIds }) { - client.updateLocationRequest(LocationRequest(listener, request.locationRequest.intervalMillis, request.locationRequest.maxUpdates, request.id), Bundle().apply { - putString("packageName", request.packageName) - putString("source", "GoogleLocationManager") - }) - activeRequestIds.add(request.id) - } - } - } - } - - override fun getLifecycle(): Lifecycle = lifecycle - - fun dump(writer: PrintWriter) { - writer.println("network provider (via direct client):") - writer.println(" last location: ${lastLocation?.let { Location(it) }}") - writer.println(" ready: $ready") - } - - companion object { - const val TAG = "GmsLocProviderU" - } -} diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/extensions.kt new file mode 100644 index 0000000000..e8ba0cda7f --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/extensions.kt @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location + +import android.location.Location +import androidx.core.location.LocationCompat +import com.google.android.gms.common.Feature + +internal val Location.elapsedMillis: Long + get() = LocationCompat.getElapsedRealtimeMillis(this) + +internal val FEATURES = arrayOf( + Feature("name_ulr_private", 1), + Feature("driving_mode", 6), + Feature("name_sleep_segment_request", 1), + Feature("support_context_feature_id", 1), + Feature("get_current_location", 2), + Feature("get_last_activity_feature_id", 1), + Feature("get_last_location_with_request", 1), + Feature("set_mock_mode_with_callback", 1), + Feature("set_mock_location_with_callback", 1), + Feature("inject_location_with_callback", 1), + Feature("location_updates_with_callback", 1), + Feature("user_service_developer_features", 1), + Feature("user_service_location_accuracy", 1), + Feature("user_service_safety_and_emergency", 1), + + Feature("use_safe_parcelable_in_intents", 1) +) \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/AbstractLocationManagerInstance.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/AbstractLocationManagerInstance.kt new file mode 100644 index 0000000000..2aa5ac8ab6 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/AbstractLocationManagerInstance.kt @@ -0,0 +1,236 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.app.PendingIntent +import android.location.Location +import android.os.IBinder +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback +import com.google.android.gms.common.internal.ICancelToken +import com.google.android.gms.location.* +import com.google.android.gms.location.internal.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + +abstract class AbstractLocationManagerInstance : IGoogleLocationManagerService.Stub() { + + override fun addGeofencesList(geofences: List, pendingIntent: PendingIntent, callbacks: IGeofencerCallbacks, packageName: String) { + val request = GeofencingRequest.Builder() + .addGeofences(geofences) + .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER or GeofencingRequest.INITIAL_TRIGGER_DWELL) + .build() + addGeofences(request, pendingIntent, callbacks) + } + + override fun requestActivityUpdates(detectionIntervalMillis: Long, triggerUpdates: Boolean, callbackIntent: PendingIntent) { + requestActivityUpdatesWithCallback(ActivityRecognitionRequest().apply { + intervalMillis = detectionIntervalMillis + triggerUpdate = triggerUpdates + }, callbackIntent, EmptyStatusCallback()) + } + + override fun getLocationAvailabilityWithPackage(packageName: String?): LocationAvailability { + val reference = AtomicReference(LocationAvailability.UNAVAILABLE) + val latch = CountDownLatch(1) + getLocationAvailabilityWithReceiver(LocationAvailabilityRequest(), LocationReceiver(object : ILocationAvailabilityStatusCallback.Stub() { + override fun onLocationAvailabilityStatus(status: Status, location: LocationAvailability) { + if (status.isSuccess) { + reference.set(location) + } + latch.countDown() + } + })) + return reference.get() + } + + override fun getCurrentLocation(request: CurrentLocationRequest, callback: ILocationStatusCallback): ICancelToken { + return getCurrentLocationWithReceiver(request, LocationReceiver(callback)) + } + + // region Last location + + override fun getLastLocation(): Location? { + val reference = AtomicReference() + val latch = CountDownLatch(1) + val request = LastLocationRequest.Builder().setMaxUpdateAgeMillis(Long.MAX_VALUE).setGranularity(Granularity.GRANULARITY_PERMISSION_LEVEL).build() + getLastLocationWithReceiver(request, LocationReceiver(object : ILocationStatusCallback.Stub() { + override fun onLocationStatus(status: Status, location: Location?) { + if (status.isSuccess) { + reference.set(location) + } + latch.countDown() + } + })) + if (latch.await(30, TimeUnit.SECONDS)) { + return reference.get() + } + return null + } + + override fun getLastLocationWithRequest(request: LastLocationRequest, callback: ILocationStatusCallback) { + getLastLocationWithReceiver(request, LocationReceiver(callback)) + } + + override fun getLastLocationWithPackage(packageName: String?): Location? { + return lastLocation + } + + override fun getLastLocationWith(s: String?): Location? { + return lastLocation + } + + // endregion + + // region Mock locations + + override fun setMockMode(mockMode: Boolean) { + val latch = CountDownLatch(1) + setMockModeWithCallback(mockMode, object : IStatusCallback.Stub() { + override fun onResult(status: Status?) { + latch.countDown() + } + }) + latch.await(30, TimeUnit.SECONDS) + } + + override fun setMockLocation(mockLocation: Location) { + val latch = CountDownLatch(1) + setMockLocationWithCallback(mockLocation, object : IStatusCallback.Stub() { + override fun onResult(status: Status?) { + latch.countDown() + } + }) + latch.await(30, TimeUnit.SECONDS) + } + + // endregion + + // region Location updates + + abstract fun registerLocationUpdates( + oldBinder: IBinder?, + binder: IBinder, + callback: ILocationCallback, + request: LocationRequest, + statusCallback: IStatusCallback + ) + + abstract fun registerLocationUpdates(pendingIntent: PendingIntent, request: LocationRequest, statusCallback: IStatusCallback) + abstract fun unregisterLocationUpdates(binder: IBinder, statusCallback: IStatusCallback) + abstract fun unregisterLocationUpdates(pendingIntent: PendingIntent, statusCallback: IStatusCallback) + + override fun requestLocationUpdatesWithCallback(receiver: LocationReceiver, request: LocationRequest, callback: IStatusCallback) { + when (receiver.type) { + LocationReceiver.TYPE_LISTENER -> registerLocationUpdates( + receiver.oldBinderReceiver, + receiver.binderReceiver!!, + receiver.listener.asCallback(), + request, + callback + ) + + LocationReceiver.TYPE_CALLBACK -> registerLocationUpdates( + receiver.oldBinderReceiver, + receiver.binderReceiver!!, + receiver.callback, + request, + callback + ) + + LocationReceiver.TYPE_PENDING_INTENT -> registerLocationUpdates(receiver.pendingIntentReceiver!!, request, callback) + else -> throw IllegalArgumentException("unknown location receiver type"); + } + } + + override fun removeLocationUpdatesWithCallback(receiver: LocationReceiver, callback: IStatusCallback) { + when (receiver.type) { + LocationReceiver.TYPE_LISTENER -> unregisterLocationUpdates(receiver.binderReceiver!!, callback) + LocationReceiver.TYPE_CALLBACK -> unregisterLocationUpdates(receiver.binderReceiver!!, callback) + LocationReceiver.TYPE_PENDING_INTENT -> unregisterLocationUpdates(receiver.pendingIntentReceiver!!, callback) + else -> throw IllegalArgumentException("unknown location receiver type"); + } + } + + override fun updateLocationRequest(data: LocationRequestUpdateData) { + val statusCallback = object : IStatusCallback.Stub() { + override fun onResult(status: Status) { + data.fusedLocationProviderCallback?.onFusedLocationProviderResult(FusedLocationProviderResult.create(status)) + } + } + when (data.opCode) { + LocationRequestUpdateData.REQUEST_UPDATES -> { + when { + data.listener != null -> registerLocationUpdates( + null, + data.listener.asBinder(), + data.listener.asCallback().redirectCancel(data.fusedLocationProviderCallback), + data.request.request, + statusCallback + ) + + data.callback != null -> registerLocationUpdates( + null, + data.callback.asBinder(), + data.callback.redirectCancel(data.fusedLocationProviderCallback), + data.request.request, + statusCallback + ) + + data.pendingIntent != null -> registerLocationUpdates(data.pendingIntent, data.request.request, statusCallback) + } + } + + LocationRequestUpdateData.REMOVE_UPDATES -> { + when { + data.listener != null -> unregisterLocationUpdates(data.listener.asBinder(), statusCallback) + data.callback != null -> unregisterLocationUpdates(data.callback.asBinder(), statusCallback) + data.pendingIntent != null -> unregisterLocationUpdates(data.pendingIntent, statusCallback) + } + } + + else -> { + statusCallback.onResult(Status(CommonStatusCodes.ERROR, "invalid location request update operation: " + data.opCode)) + } + } + } + + override fun requestLocationUpdatesWithListener(request: LocationRequest, listener: ILocationListener) { + requestLocationUpdatesWithCallback(LocationReceiver(listener), request, EmptyStatusCallback()) + } + + override fun requestLocationUpdatesWithPackage(request: LocationRequest, listener: ILocationListener, packageName: String?) { + requestLocationUpdatesWithCallback(LocationReceiver(listener), request, EmptyStatusCallback()) + } + + override fun requestLocationUpdatesWithIntent(request: LocationRequest, callbackIntent: PendingIntent) { + requestLocationUpdatesWithCallback(LocationReceiver(callbackIntent), request, EmptyStatusCallback()) + } + + override fun requestLocationUpdatesInternalWithListener(request: LocationRequestInternal, listener: ILocationListener) { + requestLocationUpdatesWithCallback(LocationReceiver(listener), request.request, EmptyStatusCallback()) + } + + override fun requestLocationUpdatesInternalWithIntent(request: LocationRequestInternal, callbackIntent: PendingIntent) { + requestLocationUpdatesWithCallback(LocationReceiver(callbackIntent), request.request, EmptyStatusCallback()) + } + + override fun removeLocationUpdatesWithListener(listener: ILocationListener) { + removeLocationUpdatesWithCallback(LocationReceiver(listener), EmptyStatusCallback()) + } + + override fun removeLocationUpdatesWithIntent(callbackIntent: PendingIntent) { + removeLocationUpdatesWithCallback(LocationReceiver(callbackIntent), EmptyStatusCallback()) + } + + // endregion + + class EmptyStatusCallback : IStatusCallback.Stub() { + override fun onResult(status: Status?) = Unit + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt new file mode 100644 index 0000000000..de03e4657e --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt @@ -0,0 +1,313 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.content.Context +import android.hardware.GeomagneticField +import android.hardware.Sensor +import android.hardware.Sensor.* +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.hardware.SensorManager.* +import android.location.Location +import android.os.Build.VERSION.SDK_INT +import android.os.Handler +import android.os.HandlerThread +import android.os.IBinder +import android.os.SystemClock +import android.os.WorkSource +import android.util.Log +import android.view.Surface +import android.view.WindowManager +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import androidx.core.location.LocationCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.location.DeviceOrientation +import com.google.android.gms.location.DeviceOrientationRequest +import com.google.android.gms.location.IDeviceOrientationListener +import com.google.android.gms.location.internal.ClientIdentity +import com.google.android.gms.location.internal.DeviceOrientationRequestInternal +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.microg.gms.utils.WorkSourceUtil +import java.io.PrintWriter +import kotlin.math.* + +class DeviceOrientationManager(private val context: Context, private val lifecycle: Lifecycle) : LifecycleOwner, SensorEventListener, IBinder.DeathRecipient { + override fun getLifecycle(): Lifecycle = lifecycle + private var lock = Mutex(false) + private var started: Boolean = false + private var handlerThread: HandlerThread? = null + private val requests = mutableMapOf() + + suspend fun add(clientIdentity: ClientIdentity, request: DeviceOrientationRequestInternal, listener: IDeviceOrientationListener) { + listener.asBinder().linkToDeath(this, 0) + lock.withLock { + requests[listener.asBinder()] = DeviceOrientationRequestHolder(clientIdentity, request.request, listener) + updateStatus() + } + } + + suspend fun remove(clientIdentity: ClientIdentity, listener: IDeviceOrientationListener) { + listener.asBinder().unlinkToDeath(this, 0) + lock.withLock { + requests.remove(listener.asBinder()) + updateStatus() + } + } + + private fun SensorManager.registerListener(sensor: Sensor, handler: Handler) { + if (SDK_INT >= 19) { + registerListener(this@DeviceOrientationManager, sensor, SAMPLING_PERIOD_US, MAX_REPORT_LATENCY_US, handler) + } else { + registerListener(this@DeviceOrientationManager, sensor, SAMPLING_PERIOD_US, handler) + } + } + + private fun updateStatus() { + if (requests.isNotEmpty() && !started) { + try { + val sensorManager = context.getSystemService() ?: return + val sensors = mutableSetOf() + if (SDK_INT >= 33) { + sensorManager.getDefaultSensor(TYPE_HEADING)?.let { sensors.add(it) } + } + if (sensors.isEmpty()) { + sensorManager.getDefaultSensor(TYPE_ROTATION_VECTOR)?.let { sensors.add(it) } + } + if (sensors.isEmpty()) { + sensors.add(sensorManager.getDefaultSensor(TYPE_MAGNETIC_FIELD) ?: return) + sensors.add(sensorManager.getDefaultSensor(TYPE_ACCELEROMETER) ?: return) + } + handlerThread = HandlerThread("DeviceOrientation") + handlerThread!!.start() + val handler = Handler(handlerThread!!.looper) + for (sensor in sensors) { + sensorManager.registerListener(sensor, handler) + } + started = true + } catch (e: Exception) { + Log.w(TAG, e) + } + } else if (requests.isEmpty() && started) { + stop() + } + } + + override fun binderDied() { + lifecycleScope.launchWhenStarted { + val toRemove = requests.keys.filter { !it.isBinderAlive }.toList() + for (binder in toRemove) { + requests.remove(binder) + } + updateStatus() + } + } + + private var location: Location? = null + fun onLocationChanged(location: Location) { + this.location = location + updateHeading() + } + + private var accelerometerValues = FloatArray(3) + private var accelerometerRealtimeNanos = 0L + private fun handleAccelerometerEvent(event: SensorEvent) { + event.values.copyInto(accelerometerValues) + accelerometerRealtimeNanos = event.timestamp + updateAzimuth() + } + + private var magneticFieldValues = FloatArray(3) + private var magneticRealtimeNanos = 0L + private fun handleMagneticEvent(event: SensorEvent) { + event.values.copyInto(magneticFieldValues) + magneticRealtimeNanos = event.timestamp + updateAzimuth() + } + + private var azimuths = FloatArray(5) + private var azimuthIndex = 0 + private var hadAzimuths = false + private var azimuth = Float.NaN + private var azimuthRealtimeNanos = 0L + private var azimuthAccuracy = Float.NaN + private fun updateAzimuth() { + if (accelerometerRealtimeNanos == 0L || magneticRealtimeNanos == 0L) return + var r = FloatArray(9) + val i = FloatArray(9) + if (getRotationMatrix(r, i, accelerometerValues, magneticFieldValues)) { + r = remapForOrientation(r) + val values = FloatArray(3) + getOrientation(r, values) + azimuths[azimuthIndex] = values[0] + if (azimuthIndex == azimuths.size - 1) { + azimuthIndex = 0 + hadAzimuths = true + } else { + azimuthIndex++ + } + var sumSin = 0.0 + var sumCos = 0.0 + for (j in 0 until (if (hadAzimuths) azimuths.size else azimuthIndex)) { + sumSin = sin(azimuths[j].toDouble()) + sumCos = cos(azimuths[j].toDouble()) + } + azimuth = Math.toDegrees(atan2(sumSin, sumCos)).toFloat() + azimuthRealtimeNanos = max(accelerometerRealtimeNanos, magneticRealtimeNanos) + updateHeading() + } + } + + private fun remapForOrientation(r: FloatArray): FloatArray { + val display = context.getSystemService()?.defaultDisplay + fun remap(x: Int, y: Int) = FloatArray(9).also { remapCoordinateSystem(r, x, y, it) } + return when (display?.rotation) { + Surface.ROTATION_90 -> remap(AXIS_Y, AXIS_MINUS_X) + Surface.ROTATION_180 -> remap(AXIS_MINUS_X, AXIS_MINUS_Y) + Surface.ROTATION_270 -> remap(AXIS_MINUS_Y, AXIS_X) + else -> r + } + } + + private fun handleRotationVectorEvent(event: SensorEvent) { + val v = FloatArray(3) + event.values.copyInto(v, endIndex = 3) + var r = FloatArray(9) + getRotationMatrixFromVector(r, v) + r = remapForOrientation(r) + val values = FloatArray(3) + getOrientation(r, values) + azimuth = Math.toDegrees(values[0].toDouble()).toFloat() + azimuthRealtimeNanos = event.timestamp + if (SDK_INT >= 18 && values.size >= 5 && values[4] != -1f) { + azimuthAccuracy = Math.toDegrees(values[4].toDouble()).toFloat() + } + updateHeading() + } + + private var heading = Float.NaN + private var headingAccuracy = Float.NaN + private var headingRealtimeNanos = 0L + private fun updateHeading() { + if (!azimuth.isNaN()) { + if (location == null) { + heading = azimuth + headingAccuracy = azimuthAccuracy.takeIf { !it.isNaN() } ?: 90.0f + headingRealtimeNanos = azimuthRealtimeNanos + } else { + heading = azimuth + location!!.run { GeomagneticField(latitude.toFloat(), longitude.toFloat(), altitude.toFloat(), time).declination } + headingAccuracy = azimuthAccuracy.takeIf { !it.isNaN() } ?: 45.0f + headingRealtimeNanos = max(LocationCompat.getElapsedRealtimeNanos(location!!), azimuthRealtimeNanos) + } + updateDeviceOrientation() + } + } + + private fun handleHeadingEvent(event: SensorEvent) { + heading = event.values[0] + headingAccuracy = event.values[1] + headingRealtimeNanos = event.timestamp + updateDeviceOrientation() + } + + private fun updateDeviceOrientation() { + val deviceOrientation = DeviceOrientation() + deviceOrientation.headingDegrees = heading + deviceOrientation.headingErrorDegrees = headingAccuracy + deviceOrientation.elapsedRealtimeNanos = headingRealtimeNanos + lifecycleScope.launchWhenStarted { + processNewDeviceOrientation(deviceOrientation) + } + } + + override fun onSensorChanged(event: SensorEvent) { + when (event.sensor.type) { + TYPE_ACCELEROMETER -> handleAccelerometerEvent(event) + TYPE_MAGNETIC_FIELD -> handleMagneticEvent(event) + TYPE_ROTATION_VECTOR -> handleRotationVectorEvent(event) + TYPE_HEADING -> handleHeadingEvent(event) + else -> return + } + } + + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { + if (sensor.type == TYPE_ROTATION_VECTOR) { + azimuthAccuracy = when (accuracy) { + SENSOR_STATUS_ACCURACY_LOW -> 45.0f + SENSOR_STATUS_ACCURACY_MEDIUM -> 30.0f + SENSOR_STATUS_ACCURACY_HIGH -> 15.0f + else -> Float.NaN + } + } + } + + fun stop() { + if (SDK_INT >= 18) handlerThread?.looper?.quitSafely() + else handlerThread?.looper?.quit() + context.getSystemService()?.unregisterListener(this) + started = false + } + + fun dump(writer: PrintWriter) { + writer.println("Current device orientation request (started=$started)") + for (request in requests.values.toList()) { + writer.println("- ${request.workSource} (pending: ${request.updatesPending} ${request.timePendingMillis}ms)") + } + } + + suspend fun processNewDeviceOrientation(deviceOrientation: DeviceOrientation) { + lock.withLock { + val toRemove = mutableSetOf() + for ((binder, holder) in requests) { + try { + holder.processNewDeviceOrientation(deviceOrientation) + } catch (e: Exception) { + toRemove.add(binder) + } + } + for (binder in toRemove) { + requests.remove(binder) + } + if (toRemove.isNotEmpty()) { + updateStatus() + } + } + } + + companion object { + const val SAMPLING_PERIOD_US = 20_000 + const val MAX_REPORT_LATENCY_US = 200_000 + + private class DeviceOrientationRequestHolder( + private val clientIdentity: ClientIdentity, + private val request: DeviceOrientationRequest, + private val listener: IDeviceOrientationListener, + ) { + private var updates = 0 + private var lastOrientation: DeviceOrientation? = null + + val updatesPending: Int + get() = request.numUpdates - updates + val timePendingMillis: Long + get() = request.expirationTime - SystemClock.elapsedRealtime() + val workSource = WorkSource().also { WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } + + fun processNewDeviceOrientation(deviceOrientation: DeviceOrientation) { + if (timePendingMillis < 0) throw RuntimeException("duration limit reached (expired at ${request.expirationTime}, now is ${SystemClock.elapsedRealtime()})") + if (lastOrientation != null && abs(lastOrientation!!.headingDegrees - deviceOrientation.headingDegrees) < Math.toDegrees(request.smallestAngleChangeRadians.toDouble())) return + if (lastOrientation == deviceOrientation) return + listener.onDeviceOrientationChanged(deviceOrientation) + updates++ + if (updatesPending <= 0) throw RuntimeException("max updates reached") + } + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LastLocationCapsule.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LastLocationCapsule.kt new file mode 100644 index 0000000000..ca553c8381 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LastLocationCapsule.kt @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.content.Context +import android.location.Location +import android.location.LocationManager +import android.os.Build.VERSION.SDK_INT +import android.os.Parcel +import android.os.Parcelable +import android.os.SystemClock +import android.util.Log +import androidx.core.content.getSystemService +import androidx.core.location.LocationCompat +import com.google.android.gms.location.Granularity +import com.google.android.gms.location.Granularity.GRANULARITY_COARSE +import com.google.android.gms.location.Granularity.GRANULARITY_FINE +import com.google.android.gms.location.LocationAvailability +import org.microg.gms.location.elapsedMillis +import org.microg.safeparcel.AutoSafeParcelable +import org.microg.safeparcel.SafeParcelUtil +import org.microg.safeparcel.SafeParcelable.Field +import java.io.File +import java.lang.Long.max +import java.util.concurrent.TimeUnit + +class LastLocationCapsule(private val context: Context) { + private var lastFineLocation: Location? = null + private var lastCoarseLocation: Location? = null + + private var lastFineLocationTimeCoarsed: Location? = null + private var lastCoarseLocationTimeCoarsed: Location? = null + + var locationAvailability: LocationAvailability = LocationAvailability.AVAILABLE + + private val file: File + get() = context.getFileStreamPath(FILE_NAME) + + fun getLocation(effectiveGranularity: @Granularity Int, maxUpdateAgeMillis: Long): Location? { + val location = when (effectiveGranularity) { + GRANULARITY_COARSE -> lastCoarseLocationTimeCoarsed + GRANULARITY_FINE -> lastCoarseLocation + else -> return null + } ?: return null + val cliff = if (effectiveGranularity == GRANULARITY_COARSE) max(maxUpdateAgeMillis, TIME_COARSE_CLIFF) else maxUpdateAgeMillis + val elapsedRealtimeDiff = SystemClock.elapsedRealtime() - LocationCompat.getElapsedRealtimeMillis(location) + if (elapsedRealtimeDiff > cliff) return null + if (elapsedRealtimeDiff <= maxUpdateAgeMillis) return location + // Location is too old according to maxUpdateAgeMillis, but still in scope due to time coarsing. Adjust time + val locationUpdated = Location(location) + val timeAdjustment = elapsedRealtimeDiff - maxUpdateAgeMillis + if (SDK_INT >= 17) { + locationUpdated.elapsedRealtimeNanos = location.elapsedRealtimeNanos + TimeUnit.MILLISECONDS.toNanos(timeAdjustment) + } + locationUpdated.time = location.time + timeAdjustment + return locationUpdated + } + + fun reset() { + lastFineLocation = null + lastFineLocationTimeCoarsed = null + lastCoarseLocation = null + lastCoarseLocationTimeCoarsed = null + locationAvailability = LocationAvailability.AVAILABLE + } + + fun updateCoarseLocation(location: Location) { + if (lastCoarseLocation != null && lastCoarseLocation!!.elapsedMillis > location.elapsedMillis + 30_000L && (!location.hasBearing() || !location.hasSpeed())) { + location.bearing = lastCoarseLocation!!.bearingTo(location) + LocationCompat.setBearingAccuracyDegrees(location, 180.0f) + location.speed = lastCoarseLocation!!.distanceTo(location) / ((location.elapsedMillis - lastCoarseLocation!!.elapsedMillis) / 1000) + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed) + } + lastCoarseLocation = newest(lastCoarseLocation, location) + lastCoarseLocationTimeCoarsed = newest(lastCoarseLocationTimeCoarsed, location, TIME_COARSE_CLIFF) + } + + fun updateFineLocation(location: Location) { + lastFineLocation = newest(lastFineLocation, location) + lastFineLocationTimeCoarsed = newest(lastFineLocationTimeCoarsed, location, TIME_COARSE_CLIFF) + updateCoarseLocation(location) + } + + private fun newest(oldLocation: Location?, newLocation: Location, cliff: Long = 0): Location { + if (oldLocation == null) return newLocation + if (LocationCompat.isMock(oldLocation) && !LocationCompat.isMock(newLocation)) return newLocation + if (LocationCompat.getElapsedRealtimeNanos(newLocation) >= LocationCompat.getElapsedRealtimeNanos(oldLocation) + TimeUnit.MILLISECONDS.toNanos(cliff)) return newLocation + return oldLocation + } + + fun start() { + try { + if (file.exists()) { + val capsule = SafeParcelUtil.fromByteArray(file.readBytes(), LastLocationCapsuleParcelable.CREATOR) + lastFineLocation = capsule.lastFineLocation + lastCoarseLocation = capsule.lastCoarseLocation + lastFineLocationTimeCoarsed = capsule.lastFineLocationTimeCoarsed + lastCoarseLocationTimeCoarsed = capsule.lastCoarseLocationTimeCoarsed + } + } catch (e: Exception) { + Log.w(TAG, e) + // Ignore + } + val locationManager = context.getSystemService() ?: return + try { + locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)?.let { updateCoarseLocation(it) } + locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)?.let { updateFineLocation(it) } + } catch (e: SecurityException) { + // Ignore + } + } + + fun stop() { + try { + if (file.exists()) file.delete() + file.writeBytes(SafeParcelUtil.asByteArray(LastLocationCapsuleParcelable(lastFineLocation, lastCoarseLocation, lastFineLocationTimeCoarsed, lastCoarseLocationTimeCoarsed))) + } catch (e: Exception) { + Log.w(TAG, e) + // Ignore + } + } + + companion object { + private const val FILE_NAME = "last_location_capsule" + private const val TIME_COARSE_CLIFF = 60_000L + + private class LastLocationCapsuleParcelable( + @Field(1) @JvmField val lastFineLocation: Location?, + @Field(2) @JvmField val lastCoarseLocation: Location?, + @Field(3) @JvmField val lastFineLocationTimeCoarsed: Location?, + @Field(4) @JvmField val lastCoarseLocationTimeCoarsed: Location? + ) : AutoSafeParcelable() { + constructor() : this(null, null, null, null) + + companion object { + @JvmField + val CREATOR = AutoCreator(LastLocationCapsuleParcelable::class.java) + } + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt new file mode 100644 index 0000000000..efd84bccc8 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt @@ -0,0 +1,253 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.Manifest +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_MUTABLE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.location.Location +import android.os.Build.VERSION.SDK_INT +import android.os.IBinder +import android.util.Log +import androidx.core.content.getSystemService +import androidx.core.location.LocationListenerCompat +import androidx.core.location.LocationManagerCompat +import androidx.core.location.LocationRequestCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.location.* +import com.google.android.gms.location.Granularity.GRANULARITY_COARSE +import com.google.android.gms.location.Granularity.GRANULARITY_FINE +import com.google.android.gms.location.internal.ClientIdentity +import org.microg.gms.location.GranularityUtil +import org.microg.gms.location.elapsedMillis +import org.microg.gms.location.network.NetworkLocationService +import java.io.PrintWriter +import kotlin.math.max +import android.location.LocationManager as SystemLocationManager + +class LocationManager(private val context: Context, private val lifecycle: Lifecycle) : LifecycleOwner { + private var coarsePendingIntent: PendingIntent? = null + private val postProcessor = LocationPostProcessor() + private val lastLocationCapsule = LastLocationCapsule(context) + private val requestManager = LocationRequestManager(context, lifecycle, postProcessor) { onRequestManagerUpdated() } + private val fineLocationListener = LocationListenerCompat { updateFineLocation(it) } + + val deviceOrientationManager = DeviceOrientationManager(context, lifecycle) + + override fun getLifecycle(): Lifecycle = lifecycle + + var started: Boolean = false + private set + + suspend fun getLastLocation(clientIdentity: ClientIdentity, request: LastLocationRequest): Location? { + if (request.maxUpdateAgeMillis < 0) throw IllegalArgumentException() + GranularityUtil.checkValidGranularity(request.granularity) + if (request.isBypass) { + val permission = if (SDK_INT >= 33) "android.permission.LOCATION_BYPASS" else Manifest.permission.WRITE_SECURE_SETTINGS + if (context.checkPermission(permission, clientIdentity.pid, clientIdentity.uid) != PackageManager.PERMISSION_GRANTED) { + throw SecurityException("Caller must hold $permission for location bypass") + } + } + if (request.impersonation != null) { + Log.w(TAG, "${clientIdentity.packageName} wants to impersonate ${request.impersonation!!.packageName}. Ignoring.") + } + val permissionGranularity = context.granularityFromPermission(clientIdentity) + val effectiveGranularity = getEffectiveGranularity(request.granularity, permissionGranularity) + val returnedLocation = if (effectiveGranularity > permissionGranularity) { + // No last location available at requested granularity due to lack of permission + null + } else { + val preLocation = lastLocationCapsule.getLocation(effectiveGranularity, request.maxUpdateAgeMillis) + val processedLocation = postProcessor.process(preLocation, effectiveGranularity, clientIdentity.isGoogle(context)) + if (!context.noteAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) { + // App Op denied + null + } else if (processedLocation != null && clientIdentity.isSelfProcess()) { + // When the request is coming from us, we want to make sure to return a new object to not accidentally modify the internal state + Location(processedLocation) + } else { + processedLocation + } + } + // TODO: Log request to local database + return returnedLocation + } + + fun getLocationAvailability(clientIdentity: ClientIdentity, request: LocationAvailabilityRequest): LocationAvailability { + if (request.bypass) { + val permission = if (SDK_INT >= 33) "android.permission.LOCATION_BYPASS" else Manifest.permission.WRITE_SECURE_SETTINGS + if (context.checkPermission(permission, clientIdentity.pid, clientIdentity.uid) != PackageManager.PERMISSION_GRANTED) { + throw SecurityException("Caller must hold $permission for location bypass") + } + } + if (request.impersonation != null) { + Log.w(TAG, "${clientIdentity.packageName} wants to impersonate ${request.impersonation!!.packageName}. Ignoring.") + } + return lastLocationCapsule.locationAvailability + } + + suspend fun addBinderRequest(clientIdentity: ClientIdentity, binder: IBinder, callback: ILocationCallback, request: LocationRequest) { + request.verify(context, clientIdentity) + requestManager.add(binder, clientIdentity, callback, request) + } + + suspend fun updateBinderRequest( + clientIdentity: ClientIdentity, + oldBinder: IBinder, + binder: IBinder, + callback: ILocationCallback, + request: LocationRequest + ) { + request.verify(context, clientIdentity) + requestManager.update(oldBinder, binder, clientIdentity, callback, request) + } + + suspend fun removeBinderRequest(binder: IBinder) { + requestManager.remove(binder) + } + + suspend fun addIntentRequest(clientIdentity: ClientIdentity, pendingIntent: PendingIntent, request: LocationRequest) { + request.verify(context, clientIdentity) + requestManager.add(pendingIntent, clientIdentity, request) + } + + suspend fun removeIntentRequest(pendingIntent: PendingIntent) { + requestManager.remove(pendingIntent) + } + + fun start() { + synchronized(this) { + if (started) return + started = true + } + val intent = Intent(context, LocationManagerService::class.java) + intent.action = NetworkLocationService.ACTION_REPORT_LOCATION + coarsePendingIntent = PendingIntent.getService(context, 0, intent, (if (SDK_INT >= 31) FLAG_MUTABLE else 0) or FLAG_UPDATE_CURRENT) + lastLocationCapsule.start() + requestManager.start() + } + + fun stop() { + synchronized(this) { + if (!started) return + started = false + } + requestManager.stop() + lastLocationCapsule.stop() + deviceOrientationManager.stop() + + val intent = Intent(context, NetworkLocationService::class.java) + intent.putExtra(NetworkLocationService.EXTRA_PENDING_INTENT, coarsePendingIntent) + intent.putExtra(NetworkLocationService.EXTRA_ENABLE, false) + context.startService(intent) + + val locationManager = context.getSystemService() ?: return + try { + LocationManagerCompat.removeUpdates(locationManager, fineLocationListener) + } catch (e: SecurityException) { + // Ignore + } + } + + private fun onRequestManagerUpdated() { + val coarseInterval = when (requestManager.granularity) { + GRANULARITY_COARSE -> max(requestManager.intervalMillis, MAX_COARSE_UPDATE_INTERVAL) + GRANULARITY_FINE -> max(requestManager.intervalMillis, MAX_FINE_UPDATE_INTERVAL) + else -> Long.MAX_VALUE + } + val fineInterval = when (requestManager.granularity) { + GRANULARITY_FINE -> requestManager.intervalMillis + else -> Long.MAX_VALUE + } + + val intent = Intent(context, NetworkLocationService::class.java) + intent.putExtra(NetworkLocationService.EXTRA_PENDING_INTENT, coarsePendingIntent) + intent.putExtra(NetworkLocationService.EXTRA_ENABLE, true) + intent.putExtra(NetworkLocationService.EXTRA_INTERVAL_MILLIS, coarseInterval) + intent.putExtra(NetworkLocationService.EXTRA_LOW_POWER, requestManager.granularity <= GRANULARITY_COARSE) + intent.putExtra(NetworkLocationService.EXTRA_WORK_SOURCE, requestManager.workSource) + context.startService(intent) + + val locationManager = context.getSystemService() ?: return + if (fineInterval != Long.MAX_VALUE) { + try { + LocationManagerCompat.requestLocationUpdates( + locationManager, + SystemLocationManager.GPS_PROVIDER, + LocationRequestCompat.Builder(fineInterval).build(), + fineLocationListener, + context.mainLooper + ) + } catch (e: SecurityException) { + // Ignore + } + } else { + try { + LocationManagerCompat.removeUpdates(locationManager, fineLocationListener) + } catch (e: SecurityException) { + // Ignore + } + } + } + + fun updateCoarseLocation(location: Location) { + val lastLocation = lastLocationCapsule.getLocation(GRANULARITY_FINE, Long.MAX_VALUE) + if (lastLocation == null || lastLocation.accuracy > location.accuracy || lastLocation.elapsedMillis + UPDATE_CLIFF_MS < location.elapsedMillis) { + lastLocationCapsule.updateCoarseLocation(location) + sendNewLocation(location) + } + } + + fun updateFineLocation(location: Location) { + lastLocationCapsule.updateFineLocation(location) + sendNewLocation(location) + } + + fun sendNewLocation(location: Location) { + lifecycleScope.launchWhenStarted { + requestManager.processNewLocation(location) + } + deviceOrientationManager.onLocationChanged(location) + } + + fun dump(writer: PrintWriter) { + writer.println("Location availability: ${lastLocationCapsule.locationAvailability}") + writer.println( + "Last coarse location: ${ + postProcessor.process( + lastLocationCapsule.getLocation(GRANULARITY_COARSE, Long.MAX_VALUE), + GRANULARITY_COARSE, + true + ) + }" + ) + writer.println( + "Last fine location: ${ + postProcessor.process( + lastLocationCapsule.getLocation(GRANULARITY_FINE, Long.MAX_VALUE), + GRANULARITY_FINE, + true + ) + }" + ) + if (requestManager.granularity > 0) { + requestManager.dump(writer) + } + deviceOrientationManager.dump(writer) + } + + companion object { + const val MAX_COARSE_UPDATE_INTERVAL = 20_000L + const val MAX_FINE_UPDATE_INTERVAL = 10_000L + const val UPDATE_CLIFF_MS = 30_000L + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerInstance.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerInstance.kt new file mode 100644 index 0000000000..26426f5b05 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerInstance.kt @@ -0,0 +1,318 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.Manifest.permission.* +import android.app.PendingIntent +import android.content.Context +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.location.Location +import android.os.IBinder +import android.os.Parcel +import android.os.SystemClock +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback +import com.google.android.gms.common.internal.ICancelToken +import com.google.android.gms.location.* +import com.google.android.gms.location.internal.* +import com.google.android.gms.location.internal.DeviceOrientationRequestUpdateData.REMOVE_UPDATES +import com.google.android.gms.location.internal.DeviceOrientationRequestUpdateData.REQUEST_UPDATES +import org.microg.gms.common.NonCancelToken +import org.microg.gms.utils.warnOnTransactionIssues + +class LocationManagerInstance( + private val context: Context, + private val locationManager: LocationManager, + private val packageName: String, + private val lifecycle: Lifecycle +) : + AbstractLocationManagerInstance(), LifecycleOwner { + + // region Geofences + + override fun addGeofences(geofencingRequest: GeofencingRequest?, pendingIntent: PendingIntent?, callbacks: IGeofencerCallbacks?) { + Log.d(TAG, "Not yet implemented: addGeofences by ${getClientIdentity().packageName}") + } + + override fun removeGeofencesByIntent(pendingIntent: PendingIntent?, callbacks: IGeofencerCallbacks?, packageName: String?) { + Log.d(TAG, "Not yet implemented: removeGeofencesByIntent by ${getClientIdentity().packageName}") + } + + override fun removeGeofencesById(geofenceRequestIds: Array?, callbacks: IGeofencerCallbacks?, packageName: String?) { + Log.d(TAG, "Not yet implemented: removeGeofencesById by ${getClientIdentity().packageName}") + } + + override fun removeAllGeofences(callbacks: IGeofencerCallbacks?, packageName: String?) { + Log.d(TAG, "Not yet implemented: removeAllGeofences by ${getClientIdentity().packageName}") + } + + // endregion + + // region Activity + + override fun getLastActivity(packageName: String?): ActivityRecognitionResult { + Log.d(TAG, "Not yet implemented: getLastActivity by ${getClientIdentity().packageName}") + return ActivityRecognitionResult(listOf(DetectedActivity(DetectedActivity.UNKNOWN, 0)), System.currentTimeMillis(), SystemClock.elapsedRealtime()) + } + + override fun requestActivityTransitionUpdates(request: ActivityTransitionRequest?, pendingIntent: PendingIntent?, callback: IStatusCallback?) { + Log.d(TAG, "Not yet implemented: requestActivityTransitionUpdates by ${getClientIdentity().packageName}") + callback?.onResult(Status.SUCCESS) + } + + override fun removeActivityTransitionUpdates(pendingIntent: PendingIntent?, callback: IStatusCallback?) { + Log.d(TAG, "Not yet implemented: removeActivityTransitionUpdates by ${getClientIdentity().packageName}") + callback?.onResult(Status.SUCCESS) + } + + override fun requestActivityUpdatesWithCallback(request: ActivityRecognitionRequest?, pendingIntent: PendingIntent?, callback: IStatusCallback?) { + Log.d(TAG, "Not yet implemented: requestActivityUpdatesWithCallback by ${getClientIdentity().packageName}") + callback?.onResult(Status.SUCCESS) + } + + override fun removeActivityUpdates(callbackIntent: PendingIntent?) { + Log.d(TAG, "Not yet implemented: removeActivityUpdates by ${getClientIdentity().packageName}") + } + + // endregion + + // region Sleep + + override fun removeSleepSegmentUpdates(pendingIntent: PendingIntent?, callback: IStatusCallback?) { + Log.d(TAG, "Not yet implemented: removeSleepSegmentUpdates by ${getClientIdentity().packageName}") + callback?.onResult(Status.SUCCESS) + } + + override fun requestSleepSegmentUpdates(pendingIntent: PendingIntent?, request: SleepSegmentRequest?, callback: IStatusCallback?) { + Log.d(TAG, "Not yet implemented: requestSleepSegmentUpdates by ${getClientIdentity().packageName}") + callback?.onResult(Status.SUCCESS) + } + + // endregion + + // region Location + + override fun flushLocations(callback: IFusedLocationProviderCallback?) { + Log.d(TAG, "flushLocations by ${getClientIdentity().packageName}") + checkHasAnyLocationPermission() + Log.d(TAG, "Not yet implemented: flushLocations") + } + + override fun getLocationAvailabilityWithReceiver(request: LocationAvailabilityRequest, receiver: LocationReceiver) { + Log.d(TAG, "getLocationAvailabilityWithReceiver by ${getClientIdentity().packageName}") + checkHasAnyLocationPermission() + val callback = receiver.availabilityStatusCallback + val clientIdentity = getClientIdentity() + lifecycleScope.launchWhenStarted { + try { + callback.onLocationAvailabilityStatus(Status.SUCCESS, locationManager.getLocationAvailability(clientIdentity, request)) + } catch (e: Exception) { + try { + callback.onLocationAvailabilityStatus(Status(CommonStatusCodes.ERROR, e.message), LocationAvailability.UNAVAILABLE) + } catch (e2: Exception) { + Log.w(TAG, "Failed", e) + } + } + } + } + + override fun getCurrentLocationWithReceiver(request: CurrentLocationRequest, receiver: LocationReceiver): ICancelToken { + Log.d(TAG, "getCurrentLocationWithReceiver by ${getClientIdentity().packageName}") + checkHasAnyLocationPermission() + Log.d(TAG, "Not yet implemented: getCurrentLocationWithReceiver") + return NonCancelToken() + } + + override fun getLastLocationWithReceiver(request: LastLocationRequest, receiver: LocationReceiver) { + Log.d(TAG, "getLastLocationWithReceiver by ${getClientIdentity().packageName}") + checkHasAnyLocationPermission() + val callback = receiver.statusCallback + val clientIdentity = getClientIdentity() + lifecycleScope.launchWhenStarted { + try { + callback.onLocationStatus(Status.SUCCESS, locationManager.getLastLocation(clientIdentity, request)) + } catch (e: Exception) { + try { + callback.onLocationStatus(Status(CommonStatusCodes.ERROR, e.message), null) + } catch (e2: Exception) { + Log.w(TAG, "Failed", e) + } + } + } + } + + override fun requestLocationSettingsDialog(settingsRequest: LocationSettingsRequest?, callback: ISettingsCallbacks?, packageName: String?) { + Log.d(TAG, "requestLocationSettingsDialog by ${getClientIdentity().packageName}") + Log.d(TAG, "Not yet implemented: requestLocationSettingsDialog") + callback?.onLocationSettingsResult(LocationSettingsResult(Status.SUCCESS)) + } + + // region Mock locations + + override fun setMockModeWithCallback(mockMode: Boolean, callback: IStatusCallback) { + Log.d(TAG, "setMockModeWithCallback by ${getClientIdentity().packageName}") + checkHasAnyLocationPermission() + val clientIdentity = getClientIdentity() + lifecycleScope.launchWhenStarted { + try { + Log.d(TAG, "Not yet implemented: setMockModeWithCallback") + callback.onResult(Status.SUCCESS) + } catch (e: Exception) { + Log.w(TAG, "Failed", e) + } + } + } + + override fun setMockLocationWithCallback(mockLocation: Location, callback: IStatusCallback) { + Log.d(TAG, "setMockLocationWithCallback by ${getClientIdentity().packageName}") + checkHasAnyLocationPermission() + val clientIdentity = getClientIdentity() + lifecycleScope.launchWhenStarted { + try { + Log.d(TAG, "Not yet implemented: setMockLocationWithCallback") + callback.onResult(Status.SUCCESS) + } catch (e: Exception) { + Log.w(TAG, "Failed", e) + } + } + } + + // endregion + + // region Location updates + + override fun registerLocationUpdates( + oldBinder: IBinder?, + binder: IBinder, + callback: ILocationCallback, + request: LocationRequest, + statusCallback: IStatusCallback + ) { + Log.d(TAG, "registerLocationUpdates (callback) by ${getClientIdentity().packageName}") + checkHasAnyLocationPermission() + val clientIdentity = getClientIdentity() + lifecycleScope.launchWhenStarted { + try { + if (oldBinder != null) { + locationManager.updateBinderRequest(clientIdentity, oldBinder, binder, callback, request) + } else { + locationManager.addBinderRequest(clientIdentity, binder, callback, request) + } + statusCallback.onResult(Status.SUCCESS) + } catch (e: Exception) { + try { + statusCallback.onResult(Status(CommonStatusCodes.ERROR, e.message)) + } catch (e2: Exception) { + Log.w(TAG, "Failed", e) + } + } + } + } + + override fun registerLocationUpdates(pendingIntent: PendingIntent, request: LocationRequest, statusCallback: IStatusCallback) { + Log.d(TAG, "registerLocationUpdates (intent) by ${getClientIdentity().packageName}") + checkHasAnyLocationPermission() + val clientIdentity = getClientIdentity() + lifecycleScope.launchWhenStarted { + try { + locationManager.addIntentRequest(clientIdentity, pendingIntent, request) + statusCallback.onResult(Status.SUCCESS) + } catch (e: Exception) { + try { + statusCallback.onResult(Status(CommonStatusCodes.ERROR, e.message)) + } catch (e2: Exception) { + Log.w(TAG, "Failed", e) + } + } + } + } + + override fun unregisterLocationUpdates(binder: IBinder, statusCallback: IStatusCallback) { + Log.d(TAG, "unregisterLocationUpdates (callback) by ${getClientIdentity().packageName}") + lifecycleScope.launchWhenStarted { + try { + locationManager.removeBinderRequest(binder) + statusCallback.onResult(Status.SUCCESS) + } catch (e: Exception) { + try { + statusCallback.onResult(Status(CommonStatusCodes.ERROR, e.message)) + } catch (e2: Exception) { + Log.w(TAG, "Failed", e) + } + } + } + } + + override fun unregisterLocationUpdates(pendingIntent: PendingIntent, statusCallback: IStatusCallback) { + Log.d(TAG, "unregisterLocationUpdates (intent) by ${getClientIdentity().packageName}") + lifecycleScope.launchWhenStarted { + try { + locationManager.removeIntentRequest(pendingIntent) + statusCallback.onResult(Status.SUCCESS) + } catch (e: Exception) { + try { + statusCallback.onResult(Status(CommonStatusCodes.ERROR, e.message)) + } catch (e2: Exception) { + Log.w(TAG, "Failed", e) + } + } + } + } + + // endregion + + // endregion + + // region Device Orientation + + override fun updateDeviceOrientationRequest(request: DeviceOrientationRequestUpdateData) { + Log.d(TAG, "updateDeviceOrientationRequest by ${getClientIdentity().packageName}") + checkHasAnyLocationPermission() + val clientIdentity = getClientIdentity() + val callback = request.fusedLocationProviderCallback + lifecycleScope.launchWhenStarted { + try { + when (request.opCode) { + REQUEST_UPDATES -> locationManager.deviceOrientationManager.add(clientIdentity, request.request, request.listener) + REMOVE_UPDATES -> locationManager.deviceOrientationManager.remove(clientIdentity, request.listener) + else -> throw UnsupportedOperationException("Op code ${request.opCode} not supported") + } + callback?.onFusedLocationProviderResult(FusedLocationProviderResult.SUCCESS) + } catch (e: Exception) { + try { + callback?.onFusedLocationProviderResult(FusedLocationProviderResult.create(Status(CommonStatusCodes.ERROR, e.message))) + } catch (e2: Exception) { + Log.w(TAG, "Failed", e) + } + } + } + } + + // endregion + + private fun getClientIdentity() = ClientIdentity(packageName).apply { uid = getCallingUid(); pid = getCallingPid() } + + private fun checkHasAnyLocationPermission() = checkHasAnyPermission(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION) + + private fun checkHasAnyPermission(vararg permissions: String) { + for (permission in permissions) { + if (context.packageManager.checkPermission(permission, packageName) == PERMISSION_GRANTED) { + return + } + } + throw SecurityException("$packageName does not have any of $permissions") + } + + override fun getLifecycle(): Lifecycle = lifecycle + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerService.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerService.kt new file mode 100644 index 0000000000..a40d8c83cd --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerService.kt @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.content.Intent +import android.location.Location +import android.os.Binder +import android.os.Process +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.location.FEATURES +import org.microg.gms.location.network.NetworkLocationService.Companion.ACTION_REPORT_LOCATION +import org.microg.gms.location.network.NetworkLocationService.Companion.EXTRA_LOCATION +import java.io.FileDescriptor +import java.io.PrintWriter + + +class LocationManagerService : BaseService(TAG, GmsService.LOCATION_MANAGER) { + private val locationManager = LocationManager(this, lifecycle) + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + locationManager.start() + if (Binder.getCallingUid() == Process.myUid() && intent?.action == ACTION_REPORT_LOCATION) { + val location = intent.getParcelableExtra(EXTRA_LOCATION) + if (location != null) { + locationManager.updateCoarseLocation(location) + } + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + locationManager.stop() + super.onDestroy() + } + + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService?) { + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + locationManager.start() + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + LocationManagerInstance(this, locationManager, packageName, lifecycle).asBinder(), + ConnectionInfo().apply { features = FEATURES } + ) + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter, args: Array?) { + super.dump(fd, writer, args) + locationManager.dump(writer) + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationPostProcessor.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationPostProcessor.kt new file mode 100644 index 0000000000..ce7da18a04 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationPostProcessor.kt @@ -0,0 +1,147 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.location.Location +import android.os.SystemClock +import androidx.core.location.LocationCompat +import com.google.android.gms.location.Granularity +import com.google.android.gms.location.Granularity.GRANULARITY_COARSE +import java.security.SecureRandom +import kotlin.math.cos +import kotlin.math.max +import kotlin.math.round + +class LocationPostProcessor { + private var nextUpdateElapsedRealtime = 0L + private val random = SecureRandom() + private var latitudeOffsetMeters = nextOffsetMeters() + private var longitudeOffsetMeters = nextOffsetMeters() + + // We cache the latest coarsed location + private var coarseLocationBefore: Location? = null + private var coarseLocationAfter: Location? = null + + private fun nextOffsetMeters(): Double = random.nextGaussian() * (COARSE_ACCURACY_METERS / 4.0) + + // We change the offset regularly to ensure there is no possibility to determine the offset and thus know exact locations when at a cliff. + private fun updateOffsetMetersIfNeeded() { + if (nextUpdateElapsedRealtime >= SystemClock.elapsedRealtime()) { + latitudeOffsetMeters = latitudeOffsetMeters * 0.97 + nextOffsetMeters() * 0.03 + longitudeOffsetMeters = longitudeOffsetMeters * 0.97 + nextOffsetMeters() * 0.03 + nextUpdateElapsedRealtime = SystemClock.elapsedRealtime() + COARSE_UPDATE_TIME + } + } + + fun process(location: Location?, granularity: @Granularity Int, forGoogle: Boolean): Location? { + if (location == null) return null + val extrasAllowList = if (forGoogle) GOOGLE_EXTRAS_LIST else PUBLIC_EXTRAS_LIST + return when { + granularity == GRANULARITY_COARSE -> { + if (location == coarseLocationBefore || location == coarseLocationAfter) { + coarseLocationAfter + } else { + val newLocation = Location(location) + newLocation.removeBearing() + newLocation.removeSpeed() + newLocation.removeAltitude() + if (LocationCompat.hasBearingAccuracy(newLocation)) LocationCompat.setBearingAccuracyDegrees(newLocation, 0f) + if (LocationCompat.hasSpeedAccuracy(newLocation)) LocationCompat.setSpeedAccuracyMetersPerSecond(newLocation, 0f) + if (LocationCompat.hasVerticalAccuracy(newLocation)) LocationCompat.setVerticalAccuracyMeters(newLocation, 0f) + newLocation.extras = null + newLocation.accuracy = max(newLocation.accuracy, COARSE_ACCURACY_METERS) + updateOffsetMetersIfNeeded() + val latitudeAccuracy = metersToLongitudeAtEquator(COARSE_ACCURACY_METERS.toDouble()) + val longitudeAccuracy = metersToLongitudeAtLatitude(COARSE_ACCURACY_METERS.toDouble(), location.latitude) + val offsetLatitude = coerceLatitude(location.latitude) + metersToLongitudeAtEquator(latitudeOffsetMeters) + newLocation.latitude = coerceLatitude(round(offsetLatitude / latitudeAccuracy) * latitudeAccuracy) + val offsetLongitude = coerceLongitude(location.longitude) + metersToLongitudeAtLatitude(longitudeOffsetMeters, newLocation.latitude) + newLocation.longitude = coerceLongitude(round(offsetLongitude / longitudeAccuracy) * longitudeAccuracy) + coarseLocationBefore = location + coarseLocationAfter = newLocation + newLocation + } + } + + location.hasAnyNonAllowedExtra(extrasAllowList) -> Location(location).stripExtras(extrasAllowList) + else -> location + } + } + + companion object { + private const val COARSE_ACCURACY_METERS = 2000f + private const val COARSE_UPDATE_TIME = 3600_000 + private const val EQUATOR_METERS_PER_LONGITUDE = 111000.0 + + val PUBLIC_EXTRAS_LIST = listOf( + "noGPSLocation", + LocationCompat.EXTRA_VERTICAL_ACCURACY, + LocationCompat.EXTRA_BEARING_ACCURACY, + LocationCompat.EXTRA_SPEED_ACCURACY, + LocationCompat.EXTRA_MSL_ALTITUDE, + LocationCompat.EXTRA_MSL_ALTITUDE_ACCURACY, + LocationCompat.EXTRA_IS_MOCK, + ) + + val GOOGLE_EXTRAS_LIST = listOf( + "noGPSLocation", + LocationCompat.EXTRA_VERTICAL_ACCURACY, + LocationCompat.EXTRA_BEARING_ACCURACY, + LocationCompat.EXTRA_SPEED_ACCURACY, + LocationCompat.EXTRA_MSL_ALTITUDE, + LocationCompat.EXTRA_MSL_ALTITUDE_ACCURACY, + LocationCompat.EXTRA_IS_MOCK, + "locationType", + "levelId", + "levelNumberE3", + "floorLabel", + "indoorProbability", + "wifiScan" + ) + + private fun Location.hasAnyNonAllowedExtra(allowList: List): Boolean { + for (key in extras?.keySet().orEmpty()) { + if (key !in allowList) { + return true + } + } + return false + } + + private fun Location.stripExtras(allowList: List): Location { + val extras = extras + for (key in extras?.keySet().orEmpty()) { + if (key !in allowList) { + extras?.remove(key) + } + } + this.extras = if (extras?.isEmpty == true) null else extras + return this + } + + private fun metersToLongitudeAtEquator(meters: Double): Double = meters / EQUATOR_METERS_PER_LONGITUDE + + private fun metersToLongitudeAtLatitude(meters: Double, latitude: Double): Double = metersToLongitudeAtEquator(meters) / cos(Math.toRadians(latitude)) + + /** + * Coerce latitude value to be between -89.99999° and 89.99999°. + * + * Sorry to those, who actually are at the geographical north/south pole, but at exactly 90°, our math wouldn't work out anymore. + */ + private fun coerceLatitude(latitude: Double): Double = latitude.coerceIn(-89.99999, 89.99999) + + /** + * Coerce longitude value to be between -180.00° and 180.00°. + */ + private fun coerceLongitude(longitude: Double): Double = (longitude % 360.0).let { + when { + it >= 180.0 -> it - 360.0 + it < -180.0 -> it + 360.0 + else -> it + } + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt new file mode 100644 index 0000000000..1b406fa1d3 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt @@ -0,0 +1,335 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.location.Location +import android.os.IBinder +import android.os.SystemClock +import android.os.WorkSource +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.location.Granularity +import com.google.android.gms.location.Granularity.GRANULARITY_FINE +import com.google.android.gms.location.Granularity.GRANULARITY_PERMISSION_LEVEL +import com.google.android.gms.location.ILocationCallback +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.internal.ClientIdentity +import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.microg.gms.location.GranularityUtil +import org.microg.gms.location.elapsedMillis +import org.microg.gms.utils.WorkSourceUtil +import java.io.PrintWriter + +class LocationRequestManager(private val context: Context, private val lifecycle: Lifecycle, private val postProcessor: LocationPostProcessor, private val requestDetailsUpdatedCallback: () -> Unit) : + IBinder.DeathRecipient, LifecycleOwner { + private val lock = Mutex() + private val binderRequests = mutableMapOf() + private val pendingIntentRequests = mutableMapOf() + var granularity: @Granularity Int = GRANULARITY_PERMISSION_LEVEL + private set + var intervalMillis: Long = Long.MAX_VALUE + private set + var workSource = WorkSource() + private set + private var requestDetailsUpdated = false + private var checkingWhileFine = false + + override fun getLifecycle(): Lifecycle = lifecycle + + override fun binderDied() { + lifecycleScope.launchWhenStarted { + lock.withLock { + val toRemove = binderRequests.keys.filter { !it.isBinderAlive }.toList() + for (binder in toRemove) { + binderRequests.remove(binder) + } + recalculateRequests() + } + notifyRequestDetailsUpdated() + } + } + + suspend fun add(binder: IBinder, clientIdentity: ClientIdentity, callback: ILocationCallback, request: LocationRequest) { + lock.withLock { + val holder = LocationRequestHolder(context, clientIdentity, request, callback, null) + try { + holder.start() + binderRequests[binder] = holder + binder.linkToDeath(this, 0) + } catch (e: Exception) { + holder.cancel() + } + recalculateRequests() + } + notifyRequestDetailsUpdated() + } + + suspend fun update(oldBinder: IBinder, binder: IBinder, clientIdentity: ClientIdentity, callback: ILocationCallback, request: LocationRequest) { + lock.withLock { + oldBinder.unlinkToDeath(this, 0) + val holder = binderRequests.remove(oldBinder) + try { + val startedHolder = holder?.update(callback, request) ?: LocationRequestHolder(context, clientIdentity, request, callback, null).start() + binderRequests[binder] = startedHolder + binder.linkToDeath(this, 0) + } catch (e: Exception) { + holder?.cancel() + } + recalculateRequests() + } + notifyRequestDetailsUpdated() + } + + suspend fun remove(oldBinder: IBinder) { + lock.withLock { + oldBinder.unlinkToDeath(this, 0) + if (binderRequests.remove(oldBinder) != null) recalculateRequests() + } + notifyRequestDetailsUpdated() + } + + suspend fun add(pendingIntent: PendingIntent, clientIdentity: ClientIdentity, request: LocationRequest) { + lock.withLock { + try { + pendingIntentRequests[pendingIntent] = LocationRequestHolder(context, clientIdentity, request, null, pendingIntent).start() + } catch (e: Exception) { + // Ignore + } + recalculateRequests() + } + notifyRequestDetailsUpdated() + } + + suspend fun remove(pendingIntent: PendingIntent) { + lock.withLock { + if (pendingIntentRequests.remove(pendingIntent) != null) recalculateRequests() + } + notifyRequestDetailsUpdated() + } + + private fun processNewLocation(location: Location, map: Map): Set { + val toRemove = mutableSetOf() + for ((key, holder) in map) { + try { + postProcessor.process(location, holder.effectiveGranularity, holder.clientIdentity.isGoogle(context))?.let { + holder.processNewLocation(it) + } + } catch (e: Exception) { + Log.w(TAG, "Exception while processing for ${holder.workSource}", e) + toRemove.add(key) + } + } + return toRemove + } + + suspend fun processNewLocation(location: Location) { + lock.withLock { + val pendingIntentsToRemove = processNewLocation(location, pendingIntentRequests) + for (pendingIntent in pendingIntentsToRemove) { + pendingIntentRequests.remove(pendingIntent) + } + val bindersToRemove = processNewLocation(location, binderRequests) + for (binder in bindersToRemove) { + try { + binderRequests[binder]?.cancel() + } catch (e: Exception) { + // Ignore + } + binderRequests.remove(binder) + } + if (pendingIntentsToRemove.isNotEmpty() || bindersToRemove.isNotEmpty()) { + recalculateRequests() + } + } + notifyRequestDetailsUpdated() + } + + private fun recalculateRequests() { + val merged = binderRequests.values + pendingIntentRequests.values + val newGranularity = merged.maxOfOrNull { it.effectiveGranularity } ?: GRANULARITY_PERMISSION_LEVEL + val newIntervalMillis = merged.minOfOrNull { it.intervalMillis } ?: Long.MAX_VALUE + val newWorkSource = WorkSource() + for (holder in merged) { + newWorkSource.add(holder.workSource) + } + if (newGranularity == GRANULARITY_FINE && granularity != GRANULARITY_FINE) lifecycleScope.launchWhenStarted { checkWhileFine() } + if (newGranularity != granularity || newIntervalMillis != intervalMillis || newWorkSource != workSource) { + granularity = newGranularity + intervalMillis = newIntervalMillis + workSource = newWorkSource + requestDetailsUpdated = true + } + } + + private suspend fun check() { + lock.withLock { + val pendingIntentsToRemove = mutableSetOf() + for ((key, holder) in pendingIntentRequests) { + try { + holder.check() + } catch (e: Exception) { + Log.w(TAG, "Exception while checking for ${holder.workSource}", e) + pendingIntentsToRemove.add(key) + } + } + for (pendingIntent in pendingIntentsToRemove) { + pendingIntentRequests.remove(pendingIntent) + } + val bindersToRemove = mutableSetOf() + for ((key, holder) in binderRequests) { + try { + holder.check() + } catch (e: Exception) { + Log.w(TAG, "Exception while checking for ${holder.workSource}", e) + bindersToRemove.add(key) + } + } + for (binder in bindersToRemove) { + try { + binderRequests[binder]?.cancel() + } catch (e: Exception) { + // Ignore + } + binderRequests.remove(binder) + } + if (pendingIntentsToRemove.isNotEmpty() || bindersToRemove.isNotEmpty()) { + recalculateRequests() + } + } + notifyRequestDetailsUpdated() + } + + private suspend fun checkWhileFine() { + if (checkingWhileFine) return + checkingWhileFine = true + while (granularity == GRANULARITY_FINE) { + check() + delay(1000) + } + checkingWhileFine = false + } + + private fun notifyRequestDetailsUpdated() { + if (!requestDetailsUpdated) return + requestDetailsUpdatedCallback() + requestDetailsUpdated = false + } + + fun stop() { + binderRequests.clear() + pendingIntentRequests.clear() + recalculateRequests() + } + + fun start() { + recalculateRequests() + notifyRequestDetailsUpdated() + } + + fun dump(writer: PrintWriter) { + writer.println("Current location request (${GranularityUtil.granularityToString(granularity)}, ${intervalMillis}ms from ${workSource})") + for (request in binderRequests.values.toList()) { + writer.println("- ${request.workSource} ${request.intervalMillis}ms ${GranularityUtil.granularityToString(request.effectiveGranularity)} (pending: ${request.updatesPending} ${request.timePendingMillis}ms)") + } + } + + companion object { + private class LocationRequestHolder( + private val context: Context, + val clientIdentity: ClientIdentity, + private var request: LocationRequest, + private var callback: ILocationCallback?, + private val pendingIntent: PendingIntent? + ) { + private var start = SystemClock.elapsedRealtime() + private var updates = 0 + private var lastLocation: Location? = null + + val permissionGranularity: @Granularity Int + get() = context.granularityFromPermission(clientIdentity) + val effectiveGranularity: @Granularity Int + get() = getEffectiveGranularity(request.granularity, permissionGranularity) + val intervalMillis: Long + get() = request.intervalMillis + val updatesPending: Int + get() = request.maxUpdates - updates + val timePendingMillis: Long + get() = request.durationMillis - (SystemClock.elapsedRealtime() - start) + var workSource: WorkSource = WorkSource(request.workSource).also { WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } + private set + + fun update(callback: ILocationCallback, request: LocationRequest): LocationRequestHolder { + val changedGranularity = request.granularity != this.request.granularity + if (changedGranularity) context.finishAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity) + this.callback = callback + this.request = request + this.start = SystemClock.elapsedRealtime() + this.updates = 0 + this.workSource = WorkSource(request.workSource).also { WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } + if (changedGranularity && !context.startAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + return this + } + + fun start(): LocationRequestHolder { + if (!context.startAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + // TODO: Register app op watch + return this + } + + fun cancel() { + try { + context.finishAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity) + callback?.cancel() + } catch (e: Exception) { + Log.w(TAG, e) + } + } + + fun check() { + if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + if (timePendingMillis < 0) throw RuntimeException("duration limit reached (active for ${SystemClock.elapsedRealtime() - start}ms, duration ${request.durationMillis}ms)") + if (updatesPending <= 0) throw RuntimeException("max updates reached") + if (callback?.asBinder()?.isBinderAlive == false) throw RuntimeException("Binder died") + } + + fun processNewLocation(location: Location) { + check() + if (lastLocation != null && location.elapsedMillis - lastLocation!!.elapsedMillis < request.minUpdateIntervalMillis) return + if (lastLocation != null && location.distanceTo(lastLocation!!) < request.minUpdateDistanceMeters) return + if (lastLocation == location) return + val returnedLocation = if (effectiveGranularity > permissionGranularity) { + throw RuntimeException("lack of permission") + } else { + if (!context.noteAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) { + throw RuntimeException("app op denied") + } else if (clientIdentity.isSelfProcess()) { + // When the request is coming from us, we want to make sure to return a new object to not accidentally modify the internal state + Location(location) + } else { + location + } + } + val result = LocationResult.create(listOf(returnedLocation)) + callback?.onLocationResult(result) + pendingIntent?.send(context, 0, Intent().apply { putExtra(LocationResult.EXTRA_LOCATION_RESULT, result) }) + updates++ + check() + } + + init { + require(callback != null || pendingIntent != null) + } + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt new file mode 100644 index 0000000000..c790141684 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/extensions.kt @@ -0,0 +1,219 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.Manifest +import android.app.AppOpsManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Binder +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.Process +import android.os.WorkSource +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.AppOpsManagerCompat +import androidx.core.content.getSystemService +import com.google.android.gms.location.* +import com.google.android.gms.location.internal.ClientIdentity +import com.google.android.gms.location.internal.IFusedLocationProviderCallback +import org.microg.gms.common.PackageUtils +import org.microg.gms.location.GranularityUtil +import org.microg.gms.utils.WorkSourceUtil + +const val TAG = "LocationManager" + +fun ILocationListener.asCallback(): ILocationCallback { + return object : ILocationCallback.Stub() { + override fun onLocationResult(result: LocationResult) { + for (location in result.locations) { + onLocationChanged(location) + } + } + + override fun onLocationAvailability(availability: LocationAvailability) = Unit + override fun cancel() = this@asCallback.cancel() + } +} + +fun ILocationCallback.redirectCancel(fusedCallback: IFusedLocationProviderCallback?): ILocationCallback { + if (fusedCallback == null) return this + return object : ILocationCallback.Stub() { + override fun onLocationResult(result: LocationResult) = this@redirectCancel.onLocationResult(result) + override fun onLocationAvailability(availability: LocationAvailability) = this@redirectCancel.onLocationAvailability(availability) + override fun cancel() = fusedCallback.cancel() + } +} + +fun ClientIdentity.isGoogle(context: Context) = PackageUtils.isGooglePackage(context, packageName) + +fun ClientIdentity.isSelfProcess() = pid == Process.myPid() + +fun Context.granularityFromPermission(clientIdentity: ClientIdentity): @Granularity Int = when (PackageManager.PERMISSION_GRANTED) { + packageManager.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION, clientIdentity.packageName) -> Granularity.GRANULARITY_FINE + packageManager.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION, clientIdentity.packageName) -> Granularity.GRANULARITY_COARSE + else -> Granularity.GRANULARITY_PERMISSION_LEVEL +} + +fun LocationRequest.verify(context: Context, clientIdentity: ClientIdentity) { + GranularityUtil.checkValidGranularity(granularity) + if (isBypass) { + val permission = if (SDK_INT >= 33) "android.permission.LOCATION_BYPASS" else Manifest.permission.WRITE_SECURE_SETTINGS + if (context.checkPermission(permission, clientIdentity.pid, clientIdentity.uid) != PackageManager.PERMISSION_GRANTED) { + throw SecurityException("Caller must hold $permission for location bypass") + } + } + if (impersonation != null) { + Log.w(TAG, "${clientIdentity.packageName} wants to impersonate ${impersonation!!.packageName}. Ignoring.") + } + +} + +@RequiresApi(19) +fun appOpFromEffectiveGranularity(effectiveGranularity: @Granularity Int) = when (effectiveGranularity) { + Granularity.GRANULARITY_FINE -> AppOpsManager.OPSTR_FINE_LOCATION + Granularity.GRANULARITY_COARSE -> AppOpsManager.OPSTR_COARSE_LOCATION + else -> throw IllegalArgumentException() +} + + +fun getEffectiveGranularity(requestGranularity: @Granularity Int, permissionGranularity: @Granularity Int) = when { + requestGranularity == Granularity.GRANULARITY_PERMISSION_LEVEL && permissionGranularity == Granularity.GRANULARITY_PERMISSION_LEVEL -> Granularity.GRANULARITY_FINE + requestGranularity == Granularity.GRANULARITY_PERMISSION_LEVEL -> permissionGranularity + else -> requestGranularity +} + +fun Context.noteAppOpForEffectiveGranularity( + clientIdentity: ClientIdentity, + effectiveGranularity: @Granularity Int, + message: String? = null +): Boolean { + return if (SDK_INT >= 19) { + try { + val op = appOpFromEffectiveGranularity(effectiveGranularity) + noteAppOp(op, clientIdentity, message) + } catch (e: Exception) { + Log.w(TAG, "Could not notify appops", e) + true + } + } else { + true + } +} + +fun Context.checkAppOpForEffectiveGranularity(clientIdentity: ClientIdentity, effectiveGranularity: @Granularity Int): Boolean { + return if (SDK_INT >= 19) { + try { + val op = appOpFromEffectiveGranularity(effectiveGranularity) + checkAppOp(op, clientIdentity) + } catch (e: Exception) { + Log.w(TAG, "Could not check appops", e) + true + } + } else { + true + } +} + +fun Context.startAppOpForEffectiveGranularity(clientIdentity: ClientIdentity, effectiveGranularity: @Granularity Int): Boolean { + return if (SDK_INT >= 19) { + try { + val op = appOpFromEffectiveGranularity(effectiveGranularity) + checkAppOp(op, clientIdentity) + } catch (e: Exception) { + Log.w(TAG, "Could not start appops", e) + true + } + } else { + true + } +} + +fun Context.finishAppOpForEffectiveGranularity(clientIdentity: ClientIdentity, effectiveGranularity: @Granularity Int) { + if (SDK_INT >= 19) { + try { + val op = appOpFromEffectiveGranularity(effectiveGranularity) + finishAppOp(op, clientIdentity) + } catch (e: Exception) { + Log.w(TAG, "Could not finish appops", e) + } + } +} + +@RequiresApi(19) +private fun Context.checkAppOp( + op: String, + clientIdentity: ClientIdentity +) = try { + if (SDK_INT >= 29) { + getSystemService()?.unsafeCheckOpNoThrow(op, clientIdentity.uid, clientIdentity.packageName) == AppOpsManager.MODE_ALLOWED + } else { + getSystemService()?.checkOpNoThrow(op, clientIdentity.uid, clientIdentity.packageName) == AppOpsManager.MODE_ALLOWED + } +} catch (e: SecurityException) { + true +} + +@RequiresApi(19) +private fun Context.startAppOp( + op: String, + clientIdentity: ClientIdentity, + message: String? +) = try { + if (SDK_INT >= 30 && clientIdentity.attributionTag != null) { + getSystemService()?.startOpNoThrow(op, clientIdentity.uid, clientIdentity.packageName, clientIdentity.attributionTag!!, message) + } else { + getSystemService()?.startOpNoThrow(op, clientIdentity.uid, clientIdentity.packageName) + } +} catch (e: SecurityException) { + if (SDK_INT >= 31) { + getSystemService()?.startProxyOpNoThrow(op, clientIdentity.uid, clientIdentity.packageName, clientIdentity.attributionTag, message) + } else { + AppOpsManager.MODE_ALLOWED + } +} == AppOpsManager.MODE_ALLOWED + +@RequiresApi(19) +private fun Context.finishAppOp( + op: String, + clientIdentity: ClientIdentity +) { + try { + if (SDK_INT >= 30 && clientIdentity.attributionTag != null) { + getSystemService()?.finishOp(op, clientIdentity.uid, clientIdentity.packageName, clientIdentity.attributionTag!!) + } else { + getSystemService()?.finishOp(op, clientIdentity.uid, clientIdentity.packageName) + } + } catch (e: SecurityException) { + if (SDK_INT >= 31) { + getSystemService()?.finishProxyOp(op, clientIdentity.uid, clientIdentity.packageName, clientIdentity.attributionTag) + } + } +} + +@RequiresApi(19) +private fun Context.noteAppOp( + op: String, + clientIdentity: ClientIdentity, + message: String? = null +) = try { + if (SDK_INT >= 30) { + getSystemService() + ?.noteOpNoThrow(op, clientIdentity.uid, clientIdentity.packageName, clientIdentity.attributionTag, message) == AppOpsManager.MODE_ALLOWED + } else { + AppOpsManagerCompat.noteOpNoThrow(this, op, clientIdentity.uid, clientIdentity.packageName) == AppOpsManager.MODE_ALLOWED + } +} catch (e: SecurityException) { + if (Binder.getCallingUid() == clientIdentity.uid) { + AppOpsManagerCompat.noteProxyOpNoThrow(this, op, clientIdentity.packageName) == AppOpsManager.MODE_ALLOWED + } else if (SDK_INT >= 29) { + getSystemService() + ?.noteProxyOpNoThrow(op, clientIdentity.packageName, clientIdentity.uid) == AppOpsManager.MODE_ALLOWED + } else { + true + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/LocationCacheDatabase.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/LocationCacheDatabase.kt new file mode 100644 index 0000000000..73b80326be --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/LocationCacheDatabase.kt @@ -0,0 +1,206 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.location.Location +import android.util.Log +import org.microg.gms.location.network.cell.CellDetails +import org.microg.gms.location.network.cell.isValid +import org.microg.gms.location.network.wifi.WifiDetails +import org.microg.gms.utils.toHexString +import java.nio.ByteBuffer + +internal class LocationCacheDatabase(context: Context?) : SQLiteOpenHelper(context, "geocache.db", null, 1) { + fun getCellLocation(cell: CellDetails): Location? { + readableDatabase.query( + TABLE_CELLS, + arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_TIME, FIELD_PRECISION), + CELLS_SELECTION, + getCellSelectionArgs(cell), + null, + null, + null + ).use { cursor -> + if (cursor.moveToNext()) { + cursor.getLocation(MAX_CELL_AGE).let { return it } + } + } + readableDatabase.query(TABLE_CELLS_PRE, arrayOf(FIELD_TIME), CELLS_PRE_SELECTION, getCellPreSelectionArgs(cell), null, null, null).use { cursor -> + if (cursor.moveToNext()) { + if (cursor.getLong(1) > System.currentTimeMillis() - MAX_CELL_AGE) { + return NEGATIVE_CACHE_ENTRY + } + } + } + return null + } + + fun putCellLocation(cell: CellDetails, location: Location) { + if (!cell.isValid) return + val cv = ContentValues().apply { + put(FIELD_MCC, cell.mcc) + put(FIELD_MNC, cell.mnc) + put(FIELD_LAC_TAC, cell.lac ?: cell.tac ?: 0) + put(FIELD_TYPE, cell.type.ordinal) + put(FIELD_CID, cell.cid) + put(FIELD_PSC, cell.psc ?: 0) + putLocation(location) + } + writableDatabase.insertWithOnConflict(TABLE_CELLS, null, cv, SQLiteDatabase.CONFLICT_REPLACE) + } + + fun getWifiScanLocation(wifis: List): Location? { + val hash = wifis.hash() ?: return null + readableDatabase.query( + TABLE_WIFI_SCANS, + arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_TIME, FIELD_PRECISION), + "$FIELD_SCAN_HASH = x'${hash.toHexString()}'", + arrayOf(), + null, + null, + null + ).use { cursor -> + if (cursor.moveToNext()) { + cursor.getLocation(MAX_WIFI_AGE).let { return it } + } + } + return null + } + + fun putWifiScanLocation(wifis: List, location: Location) { + val cv = ContentValues().apply { + put(FIELD_SCAN_HASH, wifis.hash()) + putLocation(location) + } + writableDatabase.insertWithOnConflict(TABLE_WIFI_SCANS, null, cv, SQLiteDatabase.CONFLICT_REPLACE) + } + + override fun onCreate(db: SQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS $TABLE_CELLS;") + db.execSQL("DROP TABLE IF EXISTS $TABLE_CELLS_PRE;") + db.execSQL("DROP TABLE IF EXISTS $TABLE_WIFIS;") + db.execSQL("DROP TABLE IF EXISTS $TABLE_WIFI_SCANS;") + db.execSQL("CREATE TABLE $TABLE_CELLS($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TYPE INTEGER NOT NULL, $FIELD_LAC_TAC INTEGER NOT NULL, $FIELD_CID INTEGER NOT NULL, $FIELD_PSC INTEGER NOT NULL, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") + db.execSQL("CREATE TABLE $TABLE_CELLS_PRE($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TIME INTEGER NOT NULL);") + db.execSQL("CREATE TABLE $TABLE_WIFIS($FIELD_MAC BLOB, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") + db.execSQL("CREATE TABLE $TABLE_WIFI_SCANS($FIELD_SCAN_HASH BLOB, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);") + db.execSQL("CREATE UNIQUE INDEX ${TABLE_CELLS}_index ON $TABLE_CELLS($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);") + db.execSQL("CREATE UNIQUE INDEX ${TABLE_CELLS_PRE}_index ON $TABLE_CELLS_PRE($FIELD_MCC, $FIELD_MNC);") + db.execSQL("CREATE UNIQUE INDEX ${TABLE_WIFIS}_index ON $TABLE_WIFIS($FIELD_MAC);") + db.execSQL("CREATE UNIQUE INDEX ${TABLE_WIFI_SCANS}_index ON $TABLE_WIFI_SCANS($FIELD_SCAN_HASH);") + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + onCreate(db) + } + + companion object { + private const val PROVIDER_CACHE = "cache" + val NEGATIVE_CACHE_ENTRY = Location(PROVIDER_CACHE) + private const val MAX_CELL_AGE = 1000L * 60 * 60 * 24 * 28 // 28 days + private const val MAX_WIFI_AGE = 1000L * 60 * 60 * 24 * 14 // 14 days + private const val TABLE_CELLS = "cells" + private const val TABLE_CELLS_PRE = "cells_pre" + private const val TABLE_WIFIS = "wifis" + private const val TABLE_WIFI_SCANS = "wifi_scans" + private const val FIELD_MCC = "mcc" + private const val FIELD_MNC = "mnc" + private const val FIELD_TYPE = "type" + private const val FIELD_LAC_TAC = "lac" + private const val FIELD_CID = "cid" + private const val FIELD_PSC = "psc" + private const val FIELD_LATITUDE = "lat" + private const val FIELD_LONGITUDE = "lon" + private const val FIELD_ACCURACY = "acc" + private const val FIELD_TIME = "time" + private const val FIELD_PRECISION = "prec" + private const val FIELD_MAC = "mac" + private const val FIELD_SCAN_HASH = "hash" + private const val CELLS_SELECTION = "$FIELD_MCC = ? AND $FIELD_MNC = ? AND $FIELD_TYPE = ? AND $FIELD_LAC_TAC = ? AND $FIELD_CID = ? AND $FIELD_PSC = ?" + private const val CELLS_PRE_SELECTION = "$FIELD_MCC = ? AND $FIELD_MNC = ?" + private fun getCellSelectionArgs(cell: CellDetails): Array { + return arrayOf( + cell.mcc.toString(), + cell.mnc.toString(), + cell.type.ordinal.toString(), + (cell.lac ?: cell.tac ?: 0).toString(), + cell.cid.toString(), + (cell.psc ?: 0).toString(), + ) + } + + private fun getCellPreSelectionArgs(cell: CellDetails): Array { + return arrayOf( + cell.mcc.toString(), + cell.mnc.toString() + ) + } + + private val WifiDetails.macClean: String + get() = macAddress.lowercase().replace(":", "") + + private fun List.hash(): ByteArray? { + val filtered = sortedBy { it.macClean } + .filter { it.timestamp == null || it.timestamp > System.currentTimeMillis() - 60000 } + .filter { it.signalStrength == null || it.signalStrength > -90 } + if (filtered.size < 3) return null + val maxTimestamp = maxOf { it.timestamp ?: 0L } + fun WifiDetails.hashBytes(): ByteArray { + val mac = macClean + return byteArrayOf( + mac.substring(0, 2).toInt(16).toByte(), + mac.substring(2, 4).toInt(16).toByte(), + mac.substring(4, 6).toInt(16).toByte(), + mac.substring(6, 8).toInt(16).toByte(), + mac.substring(8, 10).toInt(16).toByte(), + mac.substring(10, 12).toInt(16).toByte(), + ((maxTimestamp - (timestamp ?: 0L)) / (60 * 1000)).toByte(), // timestamp + ((signalStrength ?: 0) / 10).toByte() // signal strength + ) + } + + val buffer = ByteBuffer.allocate(filtered.size * 8) + for (wifi in filtered) { + buffer.put(wifi.hashBytes()) + } + return buffer.array().digest("SHA-256") + } + + private fun Cursor.getLocation(maxAge: Long): Location? { + if (getLong(3) > System.currentTimeMillis() - maxAge) { + if (getDouble(2) == 0.0) return NEGATIVE_CACHE_ENTRY + return Location(PROVIDER_CACHE).apply { + latitude = getDouble(0) + longitude = getDouble(1) + accuracy = getDouble(2).toFloat() + precision = getDouble(4) + } + } + return null + } + + private fun ContentValues.putLocation(location: Location) { + if (location != NEGATIVE_CACHE_ENTRY) { + put(FIELD_LATITUDE, location.latitude) + put(FIELD_LONGITUDE, location.longitude) + put(FIELD_ACCURACY, location.accuracy) + put(FIELD_TIME, location.time) + put(FIELD_PRECISION, location.precision) + } else { + put(FIELD_LATITUDE, 0.0) + put(FIELD_LONGITUDE, 0.0) + put(FIELD_ACCURACY, 0.0) + put(FIELD_TIME, System.currentTimeMillis()) + put(FIELD_PRECISION, 0.0) + } + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt new file mode 100644 index 0000000000..1bf4e2faf9 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.location.Location +import android.os.SystemClock +import android.os.WorkSource + +class NetworkLocationRequest( + var pendingIntent: PendingIntent, + var intervalMillis: Long, + var lowPower: Boolean, + var bypass: Boolean, + var workSource: WorkSource +) { + private var lastRealtime = 0L + + fun send(context: Context, location: Location) { + lastRealtime = SystemClock.elapsedRealtime() + pendingIntent.send(context, 0, Intent().apply { putExtra(NetworkLocationService.EXTRA_LOCATION, location) }) + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt new file mode 100644 index 0000000000..f69597c0c8 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt @@ -0,0 +1,291 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Intent +import android.location.Location +import android.os.Build.VERSION.SDK_INT +import android.os.Handler +import android.os.HandlerThread +import android.os.SystemClock +import android.os.WorkSource +import android.util.Log +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.android.volley.VolleyError +import kotlinx.coroutines.launch +import org.microg.gms.location.elapsedMillis +import org.microg.gms.location.network.LocationCacheDatabase.Companion.NEGATIVE_CACHE_ENTRY +import org.microg.gms.location.network.cell.CellDetails +import org.microg.gms.location.network.cell.CellDetailsCallback +import org.microg.gms.location.network.cell.CellDetailsSource +import org.microg.gms.location.network.mozilla.MozillaLocationServiceClient +import org.microg.gms.location.network.mozilla.ServiceException +import org.microg.gms.location.network.wifi.* +import java.io.FileDescriptor +import java.io.PrintWriter +import kotlin.math.min + +class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDetailsCallback { + private lateinit var handlerThread: HandlerThread + private lateinit var handler: Handler + private val activeRequests = HashSet() + private val highPowerScanRunnable = Runnable { this.scan(false) } + private val lowPowerScanRunnable = Runnable { this.scan(true) } + private val wifiDetailsSource by lazy { WifiDetailsSource.create(this, this) } + private val cellDetailsSource by lazy { CellDetailsSource.create(this, this) } + private val mozilla by lazy { MozillaLocationServiceClient(this) } + private val cache by lazy { LocationCacheDatabase(this) } + + private var lastHighPowerScanRealtime = 0L + private var lastLowPowerScanRealtime = 0L + private var highPowerIntervalMillis = Long.MAX_VALUE + private var lowPowerIntervalMillis = Long.MAX_VALUE + + private var lastWifiDetailsRealtimeMillis = 0L + private var lastCellDetailsRealtimeMillis = 0L + + private val locationLock = Any() + private var lastWifiLocation: Location? = null + private var lastCellLocation: Location? = null + private var lastLocation: Location? = null + + private val interval: Long + get() = min(highPowerIntervalMillis, lowPowerIntervalMillis) + + override fun onCreate() { + super.onCreate() + handlerThread = HandlerThread(NetworkLocationService::class.java.simpleName) + handlerThread.start() + handler = Handler(handlerThread.looper) + wifiDetailsSource.enable() + cellDetailsSource.enable() + } + + @SuppressLint("WrongConstant") + private fun scan(lowPower: Boolean) { + if (!lowPower) lastHighPowerScanRealtime = SystemClock.elapsedRealtime() + lastLowPowerScanRealtime = SystemClock.elapsedRealtime() + val workSource = synchronized(activeRequests) { activeRequests.minByOrNull { it.intervalMillis }?.workSource } + wifiDetailsSource.startScan(workSource) + cellDetailsSource.startScan(workSource) + updateRequests() + } + + private fun updateRequests(forceNow: Boolean = false, lowPower: Boolean = true) { + synchronized(activeRequests) { + lowPowerIntervalMillis = Long.MAX_VALUE + highPowerIntervalMillis = Long.MAX_VALUE + for (request in activeRequests) { + if (request.lowPower) lowPowerIntervalMillis = min(lowPowerIntervalMillis, request.intervalMillis) + else highPowerIntervalMillis = min(highPowerIntervalMillis, request.intervalMillis) + } + } + + // Low power must be strictly less than high power + if (highPowerIntervalMillis <= lowPowerIntervalMillis) lowPowerIntervalMillis = Long.MAX_VALUE + + val nextHighPowerRequestIn = + if (highPowerIntervalMillis == Long.MAX_VALUE) Long.MAX_VALUE else highPowerIntervalMillis - (SystemClock.elapsedRealtime() - lastHighPowerScanRealtime) + val nextLowPowerRequestIn = + if (lowPowerIntervalMillis == Long.MAX_VALUE) Long.MAX_VALUE else lowPowerIntervalMillis - (SystemClock.elapsedRealtime() - lastLowPowerScanRealtime) + + handler.removeCallbacks(highPowerScanRunnable) + handler.removeCallbacks(lowPowerScanRunnable) + if ((forceNow && !lowPower) || nextHighPowerRequestIn <= 0) { + Log.d(TAG, "Schedule high-power scan now") + handler.post(highPowerScanRunnable) + } else if (forceNow || nextLowPowerRequestIn <= 0) { + Log.d(TAG, "Schedule low-power scan now") + handler.post(lowPowerScanRunnable) + } else { + // Reschedule next request + if (nextLowPowerRequestIn < nextHighPowerRequestIn) { + Log.d(TAG, "Schedule low-power scan in ${nextLowPowerRequestIn}ms") + handler.postDelayed(lowPowerScanRunnable, nextLowPowerRequestIn) + } else if (nextHighPowerRequestIn != Long.MAX_VALUE) { + Log.d(TAG, "Schedule high-power scan in ${nextHighPowerRequestIn}ms") + handler.postDelayed(highPowerScanRunnable, nextHighPowerRequestIn) + } + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + handler.post { + val pendingIntent = intent?.getParcelableExtra(EXTRA_PENDING_INTENT) ?: return@post + val enable = intent.getBooleanExtra(EXTRA_ENABLE, false) + if (enable) { + val intervalMillis = intent.getLongExtra(EXTRA_INTERVAL_MILLIS, -1L) + if (intervalMillis < 0) return@post + var forceNow = intent.getBooleanExtra(EXTRA_FORCE_NOW, false) + val lowPower = intent.getBooleanExtra(EXTRA_LOW_POWER, true) + val bypass = intent.getBooleanExtra(EXTRA_BYPASS, false) + val workSource = intent.getParcelableExtra(EXTRA_WORK_SOURCE) ?: WorkSource() + synchronized(activeRequests) { + if (activeRequests.any { it.pendingIntent == pendingIntent }) { + forceNow = false + activeRequests.removeAll { it.pendingIntent == pendingIntent } + } + activeRequests.add(NetworkLocationRequest(pendingIntent, intervalMillis, lowPower, bypass, workSource)) + } + handler.post { updateRequests(forceNow, lowPower) } + } else { + synchronized(activeRequests) { + activeRequests.removeAll { it.pendingIntent == pendingIntent } + } + handler.post { updateRequests() } + } + } + super.onStartCommand(intent, flags, startId) + return START_STICKY + } + + override fun onDestroy() { + handlerThread.stop() + wifiDetailsSource.disable() + cellDetailsSource.disable() + super.onDestroy() + } + + override fun onWifiDetailsAvailable(wifis: List) { + val scanResultTimestamp = min(wifis.maxOf { it.timestamp ?: Long.MAX_VALUE }, System.currentTimeMillis()) + val scanResultRealtimeMillis = + if (SDK_INT >= 17) SystemClock.elapsedRealtime() - (System.currentTimeMillis() - scanResultTimestamp) else scanResultTimestamp + if (scanResultRealtimeMillis < lastWifiDetailsRealtimeMillis + interval / 2) { + Log.d(TAG, "Ignoring wifi details, similar age as last ($scanResultRealtimeMillis < $lastWifiDetailsRealtimeMillis + $interval / 2)") + return + } + if (wifis.size < 3) return + lastWifiDetailsRealtimeMillis = scanResultRealtimeMillis + lifecycleScope.launch { + val location = try { + when (val cacheLocation = cache.getWifiScanLocation(wifis)) { + NEGATIVE_CACHE_ENTRY -> null + null -> mozilla.retrieveMultiWifiLocation(wifis).also { + it.time = System.currentTimeMillis() + cache.putWifiScanLocation(wifis, it) + } + else -> cacheLocation + } + } catch (e: Exception) { + Log.w(TAG, "Failed retrieving location for ${wifis.size} wifi networks", e) + if (e is ServiceException && e.error.code == 404 || e is VolleyError && e.networkResponse?.statusCode == 404) { + cache.putWifiScanLocation(wifis, NEGATIVE_CACHE_ENTRY) + } + null + } ?: return@launch + location.time = scanResultTimestamp + if (SDK_INT >= 17) location.elapsedRealtimeNanos = scanResultRealtimeMillis * 1_000_000L + synchronized(locationLock) { + lastWifiLocation = location + } + sendLocationUpdate() + } + } + + override fun onCellDetailsAvailable(cells: List) { + val scanResultTimestamp = min(cells.maxOf { it.timestamp ?: Long.MAX_VALUE }, System.currentTimeMillis()) + val scanResultRealtimeMillis = + if (SDK_INT >= 17) SystemClock.elapsedRealtime() - (System.currentTimeMillis() - scanResultTimestamp) else scanResultTimestamp + if (scanResultRealtimeMillis < lastCellDetailsRealtimeMillis + interval/2) { + Log.d(TAG, "Ignoring cell details, similar age as last") + return + } + lastCellDetailsRealtimeMillis = scanResultRealtimeMillis + lifecycleScope.launch { + val singleCell = cells.filter { it.location != NEGATIVE_CACHE_ENTRY }.maxByOrNull { it.timestamp ?: it.signalStrength?.toLong() ?: 0L } ?: return@launch + val location = singleCell.location ?: try { + when (val cacheLocation = cache.getCellLocation(singleCell)) { + NEGATIVE_CACHE_ENTRY -> null + null -> mozilla.retrieveSingleCellLocation(singleCell).also { + it.time = System.currentTimeMillis() + cache.putCellLocation(singleCell, it) + } + else -> cacheLocation + } + } catch (e: Exception) { + Log.w(TAG, "Failed retrieving location for $singleCell", e) + if (e is ServiceException && e.error.code == 404 || e is VolleyError && e.networkResponse?.statusCode == 404) { + cache.putCellLocation(singleCell, NEGATIVE_CACHE_ENTRY) + } + null + } ?: return@launch + location.time = singleCell.timestamp ?: scanResultTimestamp + if (SDK_INT >= 17) location.elapsedRealtimeNanos = singleCell.timestamp?.let { SystemClock.elapsedRealtimeNanos() - (System.currentTimeMillis() - it) * 1_000_000L } ?: (scanResultRealtimeMillis * 1_000_000L) + synchronized(locationLock) { + lastCellLocation = location + } + sendLocationUpdate() + } + } + + private fun sendLocationUpdate(now: Boolean = false) { + val location = synchronized(locationLock) { + if (lastCellLocation == null && lastWifiLocation == null) return + when { + // Only non-null + lastCellLocation == null -> lastWifiLocation + lastWifiLocation == null -> lastCellLocation + // Consider cliff + lastCellLocation!!.elapsedMillis > lastWifiLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_MS -> lastCellLocation + lastWifiLocation!!.elapsedMillis > lastCellLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_MS -> lastWifiLocation + // Wifi out of cell range with higher precision + lastCellLocation!!.precision > lastWifiLocation!!.precision && lastWifiLocation!!.distanceTo(lastCellLocation!!) > 2*lastCellLocation!!.accuracy -> lastCellLocation + else -> lastWifiLocation + } + } ?: return + if (location == lastLocation) return + if (lastLocation == lastWifiLocation && location == lastCellLocation && !now) { + handler.postDelayed({ + sendLocationUpdate(true) + }, DEBOUNCE_DELAY_MS) + return + } + lastLocation = location + synchronized(activeRequests) { + for (request in activeRequests.toList()) { + try { + request.send(this@NetworkLocationService, location) + } catch (e: Exception) { + Log.w(TAG, "Pending intent error $request") + activeRequests.remove(request) + } + } + } + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter, args: Array?) { + writer.println("Last scan elapsed realtime: high-power: $lastHighPowerScanRealtime, low-power: $lastLowPowerScanRealtime") + writer.println("Last scan result time: wifi: $lastWifiDetailsRealtimeMillis, cells: $lastCellDetailsRealtimeMillis") + writer.println("Interval: high-power: ${highPowerIntervalMillis}ms, low-power: ${lowPowerIntervalMillis}ms") + writer.println("Last wifi location: $lastWifiLocation${if (lastWifiLocation == lastLocation) " (active)" else ""}") + writer.println("Last cell location: $lastCellLocation${if (lastCellLocation == lastLocation) " (active)" else ""}") + synchronized(activeRequests) { + if (activeRequests.isNotEmpty()) { + writer.println("Active requests:") + for (request in activeRequests) { + writer.println("- ${request.workSource} ${request.intervalMillis}ms (low power: ${request.lowPower}, bypass: ${request.bypass})") + } + } + } + } + + companion object { + const val ACTION_REPORT_LOCATION = "org.microg.gms.location.network.ACTION_REPORT_LOCATION" + const val EXTRA_PENDING_INTENT = "pending_intent" + const val EXTRA_ENABLE = "enable" + const val EXTRA_INTERVAL_MILLIS = "interval" + const val EXTRA_FORCE_NOW = "force_now" + const val EXTRA_LOW_POWER = "low_power" + const val EXTRA_WORK_SOURCE = "work_source" + const val EXTRA_BYPASS = "bypass" + const val EXTRA_LOCATION = "location" + const val LOCATION_TIME_CLIFF_MS = 30000L + const val DEBOUNCE_DELAY_MS = 5000L + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt new file mode 100644 index 0000000000..f6e10c4932 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.cell + +import android.location.Location + +data class CellDetails( + val type: Type, + val mcc: Int? = null, + val mnc: Int? = null, + val lac: Int? = null, + val tac: Int? = null, + val cid: Long? = null, + val sid: Int? = null, + val nid: Int? = null, + val bsid: Int? = null, + val timestamp: Long? = null, + val psc: Int? = null, + val signalStrength: Int? = null, + val location: Location? = null +) { + companion object { + enum class Type { + CDMA, GSM, WCDMA, LTE, TDSCDMA, NR + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsCallback.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsCallback.kt new file mode 100644 index 0000000000..c5672cb9b4 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsCallback.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.cell + +interface CellDetailsCallback { + fun onCellDetailsAvailable(cells: List) +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt new file mode 100644 index 0000000000..750fb9ad0c --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.cell + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build.VERSION.SDK_INT +import android.os.WorkSource +import android.telephony.CellInfo +import android.telephony.TelephonyManager +import androidx.core.content.getSystemService +import org.microg.gms.location.network.LocationCacheDatabase + +class CellDetailsSource(private val context: Context, private val callback: CellDetailsCallback) { + fun enable() = Unit + fun disable() = Unit + + @SuppressLint("MissingPermission") + fun startScan(workSource: WorkSource?) { + val telephonyManager = context.getSystemService() ?: return + if (SDK_INT >= 29) { + telephonyManager.requestCellInfoUpdate(context.mainExecutor, object : TelephonyManager.CellInfoCallback() { + override fun onCellInfo(cells: MutableList) { + val details = cells.map(CellInfo::toCellDetails).map { it.repair(context) }.filter(CellDetails::isValid) + if (details.isNotEmpty()) callback.onCellDetailsAvailable(details) + } + }) + } else if (SDK_INT >= 17) { + val details = telephonyManager.allCellInfo.map(CellInfo::toCellDetails).map { it.repair(context) }.filter(CellDetails::isValid) + if (details.isNotEmpty()) callback.onCellDetailsAvailable(details) + } else { + val networkOperator = telephonyManager.networkOperator + var mcc: Int? = null + var mnc: Int? = null + if (networkOperator != null && networkOperator.length > 4) { + mcc = networkOperator.substring(0, 3).toIntOrNull() + mnc = networkOperator.substring(3).toIntOrNull() + } + val detail = telephonyManager.cellLocation.toCellDetails(mcc, mnc) + if (detail.isValid) callback.onCellDetailsAvailable(listOf(detail)) + } + } + + companion object { + fun create(context: Context, callback: CellDetailsCallback): CellDetailsSource = CellDetailsSource(context, callback) + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt new file mode 100644 index 0000000000..e5af415602 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.cell + +import android.content.Context +import android.location.Location +import android.os.Build.VERSION.SDK_INT +import android.os.SystemClock +import android.telephony.CellIdentity +import android.telephony.CellIdentityCdma +import android.telephony.CellIdentityGsm +import android.telephony.CellIdentityLte +import android.telephony.CellIdentityNr +import android.telephony.CellIdentityTdscdma +import android.telephony.CellIdentityWcdma +import android.telephony.CellInfo +import android.telephony.CellInfoCdma +import android.telephony.CellInfoGsm +import android.telephony.CellInfoLte +import android.telephony.CellInfoTdscdma +import android.telephony.CellInfoWcdma +import android.telephony.CellLocation +import android.telephony.TelephonyManager +import android.telephony.cdma.CdmaCellLocation +import android.telephony.gsm.GsmCellLocation +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService +import org.microg.gms.location.network.TAG + +private fun locationFromCdma(latitude: Int, longitude: Int) = if (latitude == Int.MAX_VALUE || longitude == Int.MAX_VALUE) null else Location("cdma").also { + it.latitude = latitude.toDouble() / 14400.0 + it.longitude = longitude.toDouble() / 14400.0 + it.accuracy = 30000f +} + +private fun CdmaCellLocation.toCellDetails(timestamp: Long? = null) = CellDetails( + type = CellDetails.Companion.Type.CDMA, + sid = systemId, + nid = networkId, + bsid = baseStationId, + location = locationFromCdma(baseStationLatitude, baseStationLongitude), + timestamp = timestamp +) + +private fun GsmCellLocation.toCellDetails(mcc: Int? = null, mnc: Int? = null, timestamp: Long? = null) = CellDetails( + type = CellDetails.Companion.Type.GSM, + mcc = mcc, + mnc = mnc, + lac = lac.takeIf { it != Int.MAX_VALUE && it != -1 }, + cid = cid.takeIf { it != Int.MAX_VALUE && it != -1 }?.toLong(), + psc = psc.takeIf { it != Int.MAX_VALUE && it != -1 }, + timestamp = timestamp +) + +internal fun CellLocation.toCellDetails(mcc: Int? = null, mnc: Int? = null, timestamp: Long? = null) = when (this) { + is CdmaCellLocation -> toCellDetails(timestamp) + is GsmCellLocation -> toCellDetails(mcc, mnc, timestamp) + else -> throw IllegalArgumentException("Unknown CellLocation type") +} + +@RequiresApi(17) +private fun CellIdentityCdma.toCellDetails() = CellDetails( + type = CellDetails.Companion.Type.CDMA, + sid = systemId, + nid = networkId, + bsid = basestationId, + location = locationFromCdma(latitude, longitude) +) + +@RequiresApi(17) +private fun CellIdentityGsm.toCellDetails() = CellDetails( + type = CellDetails.Companion.Type.GSM, + mcc = if (SDK_INT >= 28) mccString?.toIntOrNull() else mcc.takeIf { it != Int.MAX_VALUE && it != -1 }, + mnc = if (SDK_INT >= 28) mncString?.toIntOrNull() else mnc.takeIf { it != Int.MAX_VALUE && it != -1 }, + lac = lac.takeIf { it != Int.MAX_VALUE && it != -1 }, + cid = cid.takeIf { it != Int.MAX_VALUE && it != -1 }?.toLong() +) + +@RequiresApi(18) +private fun CellIdentityWcdma.toCellDetails() = CellDetails( + type = CellDetails.Companion.Type.WCDMA, + mcc = if (SDK_INT >= 28) mccString?.toIntOrNull() else mcc.takeIf { it != Int.MAX_VALUE && it != -1 }, + mnc = if (SDK_INT >= 28) mncString?.toIntOrNull() else mnc.takeIf { it != Int.MAX_VALUE && it != -1 }, + lac = lac.takeIf { it != Int.MAX_VALUE && it != -1 }, + cid = cid.takeIf { it != Int.MAX_VALUE && it != -1 }?.toLong(), + psc = psc.takeIf { it != Int.MAX_VALUE && it != -1 } +) + +@RequiresApi(17) +private fun CellIdentityLte.toCellDetails() = CellDetails( + type = CellDetails.Companion.Type.LTE, + mcc = if (SDK_INT >= 28) mccString?.toIntOrNull() else mcc.takeIf { it != Int.MAX_VALUE && it != -1 }, + mnc = if (SDK_INT >= 28) mncString?.toIntOrNull() else mnc.takeIf { it != Int.MAX_VALUE && it != -1 }, + tac = tac.takeIf { it != Int.MAX_VALUE && it != -1 }, + cid = ci.takeIf { it != Int.MAX_VALUE && it != -1 }?.toLong() +) + +@RequiresApi(28) +private fun CellIdentityTdscdma.toCellDetails() = CellDetails( + type = CellDetails.Companion.Type.TDSCDMA, + mcc = mccString?.toIntOrNull(), + mnc = mncString?.toIntOrNull(), + lac = lac.takeIf { it != Int.MAX_VALUE && it != -1 }, + cid = cid.takeIf { it != Int.MAX_VALUE && it != -1 }?.toLong() +) + +@RequiresApi(29) +private fun CellIdentityNr.toCellDetails() = CellDetails( + type = CellDetails.Companion.Type.NR, + mcc = mccString?.toIntOrNull(), + mnc = mncString?.toIntOrNull(), + tac = tac.takeIf { it != Int.MAX_VALUE && it != -1 }, + cid = nci.takeIf { it != Long.MAX_VALUE && it != -1L } +) + +@RequiresApi(28) +internal fun CellIdentity.toCellDetails() = when { + this is CellIdentityCdma -> toCellDetails() + this is CellIdentityGsm -> toCellDetails() + this is CellIdentityWcdma -> toCellDetails() + this is CellIdentityLte -> toCellDetails() + this is CellIdentityTdscdma -> toCellDetails() + SDK_INT >= 29 && this is CellIdentityNr -> toCellDetails() + else -> throw IllegalArgumentException("Unknown CellIdentity type") +} + +private val CellInfo.epochTimestamp: Long + @RequiresApi(17) + get() = if (SDK_INT >= 30) System.currentTimeMillis() - (SystemClock.elapsedRealtime() - timestampMillis) + else System.currentTimeMillis() - (SystemClock.elapsedRealtimeNanos() - timeStamp) + +@RequiresApi(17) +internal fun CellInfo.toCellDetails() = when { + this is CellInfoCdma -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm) + this is CellInfoGsm -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm) + SDK_INT >= 18 && this is CellInfoWcdma -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm) + this is CellInfoLte -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm) + SDK_INT >= 29 && this is CellInfoTdscdma -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm) + SDK_INT >= 30 -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm) + else -> throw IllegalArgumentException("Unknown CellInfo type") +} + +/** + * Fix a few known issues in Android's parsing of MNCs + */ +internal fun CellDetails.repair(context: Context): CellDetails { + if (type == CellDetails.Companion.Type.CDMA) return this + val networkOperator = context.getSystemService()?.networkOperator ?: return this + if (networkOperator.length < 5) return this + val networkOperatorMnc = networkOperator.substring(3).toInt() + if (networkOperator[3] == '0' && mnc == null || networkOperator.length == 5 && mnc == networkOperatorMnc * 10 + 15) + return copy(mnc = networkOperatorMnc) + return this +} + +val CellDetails.isValid: Boolean + get() = when (type) { + CellDetails.Companion.Type.CDMA -> sid != null && nid != null && bsid != null + else -> mcc != null && mnc != null && cid != null && (lac != null || tac != null) + } \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/extensions.kt new file mode 100644 index 0000000000..e20e43ada8 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/extensions.kt @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network + +import android.location.Location +import android.os.Bundle +import androidx.core.location.LocationCompat +import java.security.MessageDigest + +const val TAG = "NetworkLocation" +internal const val LOCATION_EXTRA_PRECISION = "precision" + +internal var Location.precision: Double + get() = extras?.getDouble(LOCATION_EXTRA_PRECISION, 1.0) ?: 1.0 + set(value) { + extras = (extras ?: Bundle()).apply { putDouble(LOCATION_EXTRA_PRECISION, value) } + } + +fun ByteArray.toHexString(separator: String = "") : String = joinToString(separator) { "%02x".format(it) } +fun ByteArray.digest(md: String): ByteArray = MessageDigest.getInstance(md).digest(this) \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/CellTower.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/CellTower.kt new file mode 100644 index 0000000000..6c93d5687a --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/CellTower.kt @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +data class CellTower( + /** + * The type of radio network. + */ + val radioType: RadioType? = null, + /** + * The mobile country code. + */ + val mobileCountryCode: Int? = null, + /** + * The mobile network code. + */ + val mobileNetworkCode: Int? = null, + /** + * The location area code for GSM and WCDMA networks. The tracking area code for LTE networks. + */ + val locationAreaCode: Int? = null, + /** + * The cell id or cell identity. + */ + val cellId: Int? = null, + /** + * The number of milliseconds since this networks was last detected. + */ + val age: Long? = null, + /** + * The primary scrambling code for WCDMA and physical cell id for LTE. + */ + val psc: Int? = null, + /** + * The signal strength for this cell network, either the RSSI or RSCP. + */ + val signalStrength: Int? = null, + /** + * The timing advance value for this cell network. + */ + val timingAdvance: Int? = null, +) \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/Fallback.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/Fallback.kt new file mode 100644 index 0000000000..6eb2d4b2be --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/Fallback.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +/** + * By default, both a GeoIP based position fallback and a fallback based on cell location areas (lac’s) are enabled. Omit the fallbacks section if you want to use the defaults. Change the values to false if you want to disable either of the fallbacks. + */ +data class Fallback( + /** + * If no exact cell match can be found, fall back from exact cell position estimates to more coarse grained cell location area estimates rather than going directly to an even worse GeoIP based estimate. + */ + val lacf: Boolean? = null, + /** + * If no position can be estimated based on any of the provided data points, fall back to an estimate based on a GeoIP database based on the senders IP address at the time of the query. + */ + val ipf: Boolean? = null +) \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateRequest.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateRequest.kt new file mode 100644 index 0000000000..9f2fdf8825 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateRequest.kt @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +data class GeolocateRequest( + /** + * The clear text name of the cell carrier / operator. + */ + val carrier: String? = null, + /** + * Should the clients IP address be used to locate it; defaults to true. + */ + val considerIp: Boolean? = null, + /** + * The mobile country code stored on the SIM card. + */ + val homeMobileCountryCode: Int? = null, + /** + * The mobile network code stored on the SIM card. + */ + val homeMobileNetworkCode: Int? = null, + /** + * Same as the `radioType` entry in each cell record. If all the cell entries have the same `radioType`, it can be provided at the top level instead. + */ + val radioType: RadioType? = null, + val cellTowers: List? = null, + val wifiAccessPoints: List? = null, + val fallbacks: Fallback? = null, +) \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateResponse.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateResponse.kt new file mode 100644 index 0000000000..07fa05600e --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/GeolocateResponse.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +data class GeolocateResponse( + val location: ResponseLocation?, + val accuracy: Double?, + val fallback: String?, + val error: ResponseError? +) \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt new file mode 100644 index 0000000000..1138b101b6 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +import android.content.Context +import android.location.Location +import android.os.Bundle +import android.util.Log +import com.android.volley.Request.Method +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import org.microg.gms.location.network.NetworkLocationService +import org.microg.gms.location.network.cell.CellDetails +import org.microg.gms.location.network.precision +import org.microg.gms.location.network.wifi.WifiDetails +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlin.math.log10 +import kotlin.math.log2 + +class MozillaLocationServiceClient(context: Context) { + private val queue = Volley.newRequestQueue(context) + + suspend fun retrieveMultiWifiLocation(wifis: List): Location = geoLocate( + GeolocateRequest( + considerIp = false, + wifiAccessPoints = wifis.filter { it.ssid?.endsWith("_nomap") != true }.map(WifiDetails::toWifiAccessPoint), + fallbacks = Fallback(lacf = false, ipf = false) + ) + ).apply { + precision = WIFI_BASE_PRECISION_COUNT/wifis.size.toDouble() + } + + suspend fun retrieveSingleCellLocation(cell: CellDetails): Location = geoLocate( + GeolocateRequest( + considerIp = false, + cellTowers = listOf(cell.toCellTower()), + fallbacks = Fallback( + lacf = true, + ipf = false + ) + ) + ).apply { + precision = if (extras?.getString(LOCATION_EXTRA_FALLBACK) != null) CELL_FALLBACK_PRECISION else CELL_DEFAULT_PRECISION + } + + private suspend fun geoLocate(request: GeolocateRequest): Location { + val response = rawGeoLocate(request) + Log.d(TAG, "$request -> $response") + if (response.location != null) { + return Location("mozilla").apply { + latitude = response.location.lat + longitude = response.location.lng + if (response.accuracy != null) accuracy = response.accuracy.toFloat() + if (response.fallback != null) extras = Bundle().apply { putString(LOCATION_EXTRA_FALLBACK, response.fallback) } + } + } else if (response.error != null) { + throw ServiceException(response.error) + } else { + throw RuntimeException("Invalid response JSON") + } + } + + private suspend fun rawGeoLocate(request: GeolocateRequest): GeolocateResponse = suspendCoroutine { continuation -> + queue.add(JsonObjectRequest(Method.POST, GEOLOCATE_URL.format(API_KEY), request.toJson(), { + continuation.resume(it.toGeolocateResponse()) + }, { + continuation.resumeWithException(it) + })) + } + + companion object { + private const val TAG = "MozillaLocation" + private const val GEOLOCATE_URL = "https://location.services.mozilla.com/v1/geolocate?key=%s" + private const val API_KEY = "068ab754-c06b-473d-a1e5-60e7b1a2eb77" + private const val WIFI_BASE_PRECISION_COUNT = 8.0 + private const val CELL_DEFAULT_PRECISION = 1.0 + private const val CELL_FALLBACK_PRECISION = 0.5 + const val LOCATION_EXTRA_FALLBACK = "fallback" + } + +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/RadioType.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/RadioType.kt new file mode 100644 index 0000000000..b9e8555a69 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/RadioType.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +enum class RadioType { + GSM, WCDMA, LTE; + + override fun toString(): String { + return super.toString().lowercase() + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseError.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseError.kt new file mode 100644 index 0000000000..3753b7aeb1 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseError.kt @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +data class ResponseError( + val code: Int, + val message: String +) \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseLocation.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseLocation.kt new file mode 100644 index 0000000000..cc904f5550 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ResponseLocation.kt @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +data class ResponseLocation( + val lat: Double, + val lng: Double +) \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ServiceException.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ServiceException.kt new file mode 100644 index 0000000000..3cdfbc7546 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/ServiceException.kt @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +class ServiceException(val error: ResponseError) : Exception(error.message) \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/WifiAccessPoint.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/WifiAccessPoint.kt new file mode 100644 index 0000000000..3959e1972a --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/WifiAccessPoint.kt @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +data class WifiAccessPoint( + /** + * The BSSID of the WiFi network. + */ + val macAddress: String, + /** + * The number of milliseconds since this network was last detected. + */ + val age: Long? = null, + /** + * The WiFi channel for networks in the 2.4GHz range. This often ranges from 1 to 13. + */ + val channel: Int? = null, + /** + * The frequency in MHz of the channel over which the client is communicating with the access point. + */ + val frequency: Int? = null, + /** + * The received signal strength (RSSI) in dBm. + */ + val signalStrength: Int? = null, + /** + * The current signal to noise ratio measured in dB. + */ + val signalToNoiseRatio: Int? = null, + /** + * The SSID of the Wifi network. + */ + val ssid: String? = null +) { + init { + if (ssid != null && ssid.endsWith("_nomap")) throw IllegalArgumentException("Wifi networks with a SSID ending in _nomap must not be collected.") + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/extensions.kt new file mode 100644 index 0000000000..d8c40e728e --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/mozilla/extensions.kt @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +import org.json.JSONArray +import org.json.JSONObject +import org.microg.gms.location.network.cell.CellDetails +import org.microg.gms.location.network.wifi.WifiDetails + +internal fun CellDetails.toCellTower() = CellTower( + radioType = when (type) { + CellDetails.Companion.Type.GSM -> RadioType.GSM + CellDetails.Companion.Type.WCDMA -> RadioType.WCDMA + CellDetails.Companion.Type.LTE -> RadioType.LTE + else -> throw IllegalArgumentException("Unsupported radio type") + }, + mobileCountryCode = mcc, + mobileNetworkCode = mnc, + locationAreaCode = lac ?: tac, + cellId = cid?.toInt(), + age = timestamp?.let { System.currentTimeMillis() - it }, + psc = psc, + signalStrength = signalStrength +) + +internal fun WifiDetails.toWifiAccessPoint() = WifiAccessPoint( + macAddress = macAddress, + age = timestamp?.let { System.currentTimeMillis() - it }, + channel = channel, + frequency = frequency, + signalStrength = signalStrength, + ssid = ssid +) + +internal fun JSONObject.toGeolocateResponse() = GeolocateResponse( + location = optJSONObject("location")?.toResponseLocation(), + accuracy = optDouble("accuracy"), + fallback = optString("fallback").takeIf { it.isNotEmpty() }, + error = optJSONObject("error")?.toResponseError() +) + +internal fun GeolocateRequest.toJson() = JSONObject().apply { + if (carrier != null) put("carrier", carrier) + if (considerIp != null) put("considerIp", considerIp) + if (homeMobileCountryCode != null) put("homeMobileCountryCode", homeMobileCountryCode) + if (homeMobileNetworkCode != null) put("homeMobileNetworkCode", homeMobileNetworkCode) + if (radioType != null) put("radioType", radioType.toString()) + if (!cellTowers.isNullOrEmpty()) put("cellTowers", JSONArray(cellTowers.map(CellTower::toJson))) + if (!wifiAccessPoints.isNullOrEmpty()) put("wifiAccessPoints", JSONArray(wifiAccessPoints.map(WifiAccessPoint::toJson))) + if (fallbacks != null) put("fallbacks", fallbacks.toJson()) +} + +private fun JSONObject.toResponseLocation() = ResponseLocation( + lat = getDouble("lat"), + lng = getDouble("lng") +) + +private fun JSONObject.toResponseError() = ResponseError( + code = getInt("code"), + message = getString("message") +) + +private fun CellTower.toJson() = JSONObject().apply { + if (radioType != null) put("radioType", radioType.toString()) + if (mobileCountryCode != null) put("mobileCountryCode", mobileCountryCode) + if (mobileNetworkCode != null) put("mobileNetworkCode", mobileNetworkCode) + if (locationAreaCode != null) put("locationAreaCode", locationAreaCode) + if (cellId != null) put("cellId", cellId) + if (age != null) put("age", age) + if (psc != null) put("psc", psc) + if (signalStrength != null) put("signalStrength", signalStrength) + if (timingAdvance != null) put("timingAdvance", timingAdvance) +} + +private fun WifiAccessPoint.toJson() = JSONObject().apply { + put("macAddress", macAddress) + if (age != null) put("age", age) + if (channel != null) put("channel", channel) + if (frequency != null) put("frequency", frequency) + if (signalStrength != null) put("signalStrength", signalStrength) + if (signalToNoiseRatio != null) put("signalToNoiseRatio", signalToNoiseRatio) + if (ssid != null) put("ssid", ssid) +} + +private fun Fallback.toJson() = JSONObject().apply { + if (lacf != null) put("lacf", lacf) + if (ipf != null) put("ipf", ipf) +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt new file mode 100644 index 0000000000..4ed0f6e151 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.wifi + +data class WifiDetails( + val macAddress: String, + val ssid: String? = null, + val timestamp: Long? = null, + val frequency: Int? = null, + val channel: Int? = null, + val signalStrength: Int? = null +) diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt new file mode 100644 index 0000000000..79a54c8ca4 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.wifi + +interface WifiDetailsCallback { + fun onWifiDetailsAvailable(wifis: List) +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsSource.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsSource.kt new file mode 100644 index 0000000000..ed9e6e0a25 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsSource.kt @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.wifi + +import android.content.Context +import android.os.WorkSource + +interface WifiDetailsSource { + fun enable() = Unit + fun disable() = Unit + fun startScan(workSource: WorkSource?) = Unit + + companion object { + fun create(context: Context, callback: WifiDetailsCallback) = when { + WifiScannerSource.isSupported(context) -> WifiScannerSource(context, callback) + else -> WifiManagerSource(context, callback) + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt new file mode 100644 index 0000000000..faa6d4d5de --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.wifi + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.os.WorkSource +import android.util.Log +import androidx.core.content.getSystemService +import org.microg.gms.location.network.TAG + +class WifiManagerSource(private val context: Context, private val callback: WifiDetailsCallback) : BroadcastReceiver(), WifiDetailsSource { + + @SuppressLint("MissingPermission") + override fun onReceive(context: Context, intent: Intent) { + try { + callback.onWifiDetailsAvailable(this.context.getSystemService()?.scanResults.orEmpty().map(ScanResult::toWifiDetails)) + } catch (e: Exception) { + Log.w(TAG, e) + } + } + + override fun enable() { + context.registerReceiver(this, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) + } + + override fun disable() { + context.unregisterReceiver(this) + } + + override fun startScan(workSource: WorkSource?) { + context.getSystemService()?.startScan() + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt new file mode 100644 index 0000000000..fb4407b1b5 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.wifi + +import android.content.Context +import android.net.wifi.ScanResult +import android.net.wifi.WifiScanner +import android.os.WorkSource +import android.util.Log +import org.microg.gms.location.network.TAG + +class WifiScannerSource(private val context: Context, private val callback: WifiDetailsCallback) : WifiDetailsSource { + override fun startScan(workSource: WorkSource?) { + val scanner = context.getSystemService("wifiscanner") as WifiScanner + scanner.startScan(WifiScanner.ScanSettings(), object : WifiScanner.ScanListener { + override fun onSuccess() { + Log.d(TAG, "Not yet implemented: onSuccess") + } + + override fun onFailure(reason: Int, description: String?) { + Log.d(TAG, "Not yet implemented: onFailure") + } + + override fun onPeriodChanged(periodInMs: Int) { + Log.d(TAG, "Not yet implemented: onPeriodChanged") + } + + override fun onResults(results: Array) { + callback.onWifiDetailsAvailable(results.flatMap { it.results.toList() }.map(ScanResult::toWifiDetails)) + } + + override fun onFullResult(fullScanResult: ScanResult) { + Log.d(TAG, "Not yet implemented: onFullResult") + } + }, workSource) + } + + companion object { + fun isSupported(context: Context): Boolean { + return (context.getSystemService("wifiscanner") as? WifiScanner) != null + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt new file mode 100644 index 0000000000..560f11ad97 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.wifi + +import android.net.wifi.ScanResult +import android.os.Build +import android.os.SystemClock + +internal fun ScanResult.toWifiDetails(): WifiDetails = WifiDetails( + macAddress = BSSID, + ssid = SSID, + timestamp = if (Build.VERSION.SDK_INT >= 19) System.currentTimeMillis() - (SystemClock.elapsedRealtime() - (timestamp / 1000)) else null, + frequency = frequency, + channel = frequencyToChannel(frequency), + signalStrength = level +) + +private const val BAND_24_GHZ_FIRST_CH_NUM = 1 +private const val BAND_24_GHZ_LAST_CH_NUM = 14 +private const val BAND_5_GHZ_FIRST_CH_NUM = 32 +private const val BAND_5_GHZ_LAST_CH_NUM = 177 +private const val BAND_6_GHZ_FIRST_CH_NUM = 1 +private const val BAND_6_GHZ_LAST_CH_NUM = 233 +private const val BAND_60_GHZ_FIRST_CH_NUM = 1 +private const val BAND_60_GHZ_LAST_CH_NUM = 6 +private const val BAND_24_GHZ_START_FREQ_MHZ = 2412 +private const val BAND_24_GHZ_END_FREQ_MHZ = 2484 +private const val BAND_5_GHZ_START_FREQ_MHZ = 5160 +private const val BAND_5_GHZ_END_FREQ_MHZ = 5885 +private const val BAND_6_GHZ_OP_CLASS_136_CH_2_FREQ_MHZ = 5935 +private const val BAND_6_GHZ_START_FREQ_MHZ = 5955 +private const val BAND_6_GHZ_END_FREQ_MHZ = 7115 +private const val BAND_60_GHZ_START_FREQ_MHZ = 58320 +private const val BAND_60_GHZ_END_FREQ_MHZ = 70200 + +internal fun frequencyToChannel(freq: Int): Int? { + return when (freq) { + // Special cases + BAND_24_GHZ_END_FREQ_MHZ -> BAND_24_GHZ_LAST_CH_NUM + BAND_6_GHZ_OP_CLASS_136_CH_2_FREQ_MHZ -> 2 + + in BAND_24_GHZ_START_FREQ_MHZ..BAND_24_GHZ_END_FREQ_MHZ -> + (freq - BAND_24_GHZ_START_FREQ_MHZ) / 5 + BAND_24_GHZ_FIRST_CH_NUM + + in BAND_5_GHZ_START_FREQ_MHZ..BAND_5_GHZ_END_FREQ_MHZ -> + (freq - BAND_5_GHZ_START_FREQ_MHZ) / 5 + BAND_5_GHZ_FIRST_CH_NUM + + in BAND_6_GHZ_START_FREQ_MHZ..BAND_6_GHZ_END_FREQ_MHZ -> + (freq - BAND_6_GHZ_START_FREQ_MHZ) / 5 + BAND_6_GHZ_FIRST_CH_NUM + + in BAND_60_GHZ_START_FREQ_MHZ..BAND_60_GHZ_END_FREQ_MHZ -> + (freq - BAND_60_GHZ_START_FREQ_MHZ) / 2160 + BAND_60_GHZ_FIRST_CH_NUM + + else -> null + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt new file mode 100644 index 0000000000..74cd11c337 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.content.Context +import android.location.Location +import android.location.LocationProvider +import android.os.Bundle +import android.os.SystemClock +import android.util.Log +import androidx.annotation.RequiresApi +import com.android.location.provider.LocationProviderBase +import com.android.location.provider.ProviderPropertiesUnbundled +import java.io.FileDescriptor +import java.io.PrintWriter + +abstract class AbstractLocationProviderPreTiramisu : LocationProviderBase, GenericLocationProvider { + @Deprecated("Use only with SDK < 31") + constructor(properties: ProviderPropertiesUnbundled) : super(TAG, properties) + + @RequiresApi(31) + constructor(context: Context, properties: ProviderPropertiesUnbundled) : super(context, TAG, properties) + + private var statusUpdateTime = SystemClock.elapsedRealtime() + + override fun onDump(fd: FileDescriptor, pw: PrintWriter, args: Array) { + dump(pw) + } + + override fun dump(writer: PrintWriter) { + // Nothing by default + } + + override fun onFlush(callback: OnFlushCompleteCallback?) { + Log.d(TAG, "onFlush") + callback!!.onFlushComplete() + } + + override fun onSendExtraCommand(command: String?, extras: Bundle?): Boolean { + Log.d(TAG, "onSendExtraCommand $command $extras") + return false + } + + @Deprecated("Overriding this is required pre-Q, but not used since Q") + override fun onEnable() { + Log.d(TAG, "onEnable") + statusUpdateTime = SystemClock.elapsedRealtime() + } + + @Deprecated("Overriding this is required pre-Q, but not used since Q") + override fun onDisable() { + Log.d(TAG, "onDisable") + statusUpdateTime = SystemClock.elapsedRealtime() + } + + @Deprecated("Overriding this is required pre-Q, but not used since Q") + override fun onGetStatus(extras: Bundle?): Int { + Log.d(TAG, "onGetStatus $extras") + return LocationProvider.AVAILABLE + } + + + @Deprecated("Overriding this is required pre-Q, but not used since Q") + override fun onGetStatusUpdateTime(): Long { + Log.d(TAG, "onGetStatusUpdateTime") + return statusUpdateTime + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt new file mode 100644 index 0000000000..4f31ffc344 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.location.Location +import android.os.IBinder +import java.io.PrintWriter + +interface GenericLocationProvider { + fun getBinder(): IBinder + fun enable() + fun disable() + fun dump(writer: PrintWriter) + fun reportLocation(location: Location) +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt new file mode 100644 index 0000000000..a99e3814b7 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.app.Service +import android.content.Intent +import android.os.IBinder + +class GeocodeProviderService : Service() { + private var bound: Boolean = false + private var provider: OpenStreetMapNominatimGeocodeProvider? = null + + override fun onBind(intent: Intent?): IBinder? { + if (provider == null) { + provider = OpenStreetMapNominatimGeocodeProvider(this) + } + bound = true + return provider?.binder + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt new file mode 100644 index 0000000000..c66ff22bd9 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_MUTABLE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.content.Context +import android.content.Intent +import android.location.Criteria +import android.location.Location +import android.os.Build.VERSION.SDK_INT +import android.os.WorkSource +import android.util.Log +import androidx.annotation.RequiresApi +import com.android.location.provider.ProviderPropertiesUnbundled +import com.android.location.provider.ProviderRequestUnbundled +import org.microg.gms.location.network.LOCATION_EXTRA_PRECISION +import org.microg.gms.location.network.NetworkLocationService +import org.microg.gms.location.network.NetworkLocationService.Companion.ACTION_REPORT_LOCATION +import java.io.PrintWriter +import kotlin.math.max + +class NetworkLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu { + @Deprecated("Use only with SDK < 31") + constructor(context: Context, legacy: Unit) : super(properties) { + this.context = context + } + + @RequiresApi(31) + constructor(context: Context) : super(context, properties) { + this.context = context + } + + private val context: Context + private var enabled = false + private var currentRequest: ProviderRequestUnbundled? = null + private var pendingIntent: PendingIntent? = null + private var lastReportedLocation: Location? = null + + private fun updateRequest() { + if (enabled) { + val forceNow: Boolean + val intervalMillis: Long + if (currentRequest?.reportLocation == true) { + forceNow = true + intervalMillis = max(currentRequest?.interval ?: Long.MAX_VALUE, MIN_INTERVAL_MILLIS) + } else { + forceNow = false + intervalMillis = Long.MAX_VALUE + } + val intent = Intent(context, NetworkLocationService::class.java) + intent.putExtra(NetworkLocationService.EXTRA_PENDING_INTENT, pendingIntent) + intent.putExtra(NetworkLocationService.EXTRA_ENABLE, true) + intent.putExtra(NetworkLocationService.EXTRA_INTERVAL_MILLIS, intervalMillis) + intent.putExtra(NetworkLocationService.EXTRA_FORCE_NOW, forceNow) + if (SDK_INT >= 31) { + intent.putExtra(NetworkLocationService.EXTRA_LOW_POWER, currentRequest?.isLowPower ?: false) + intent.putExtra(NetworkLocationService.EXTRA_WORK_SOURCE, currentRequest?.workSource) + } + if (SDK_INT >= 29) { + intent.putExtra(NetworkLocationService.EXTRA_BYPASS, currentRequest?.isLocationSettingsIgnored ?: false) + } + context.startService(intent) + } + } + + override fun dump(writer: PrintWriter) { + writer.println("Enabled: $enabled") + writer.println("Current request: $currentRequest") + writer.println("Last reported: $lastReportedLocation") + } + + override fun onSetRequest(request: ProviderRequestUnbundled, source: WorkSource) { + synchronized(this) { + currentRequest = request + updateRequest() + } + } + + override fun enable() { + synchronized(this) { + if (enabled) throw IllegalStateException() + val intent = Intent(context, NetworkLocationProviderService::class.java) + intent.action = ACTION_REPORT_LOCATION + pendingIntent = PendingIntent.getService(context, 0, intent, (if (SDK_INT >= 31) FLAG_MUTABLE else 0) or FLAG_UPDATE_CURRENT) + currentRequest = null + enabled = true + when { + SDK_INT >= 30 -> isAllowed = true + SDK_INT >= 29 -> isEnabled = true + } + } + } + + override fun disable() { + synchronized(this) { + if (!enabled) throw IllegalStateException() + val intent = Intent(context, NetworkLocationService::class.java) + intent.putExtra(NetworkLocationService.EXTRA_PENDING_INTENT, pendingIntent) + intent.putExtra(NetworkLocationService.EXTRA_ENABLE, false) + context.startService(intent) + pendingIntent?.cancel() + pendingIntent = null + currentRequest = null + enabled = false + } + } + + override fun reportLocation(location: Location) { + location.provider = "network" + location.extras?.remove(LOCATION_EXTRA_PRECISION) + lastReportedLocation = location + super.reportLocation(location) + } + + companion object { + private const val MIN_INTERVAL_MILLIS = 20000L + private val properties = ProviderPropertiesUnbundled.create(false, false, false, false, true, true, true, Criteria.POWER_LOW, Criteria.ACCURACY_COARSE) + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt new file mode 100644 index 0000000000..50fa462e06 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.app.Service +import android.content.Intent +import android.location.Location +import android.os.* +import android.os.Build.VERSION.SDK_INT +import org.microg.gms.location.network.NetworkLocationService.Companion.ACTION_REPORT_LOCATION +import org.microg.gms.location.network.NetworkLocationService.Companion.EXTRA_LOCATION +import java.io.FileDescriptor +import java.io.PrintWriter + +class NetworkLocationProviderService : Service() { + private lateinit var handlerThread: HandlerThread + private lateinit var handler: Handler + private var bound: Boolean = false + private var provider: GenericLocationProvider? = null + + override fun onCreate() { + super.onCreate() + handlerThread = HandlerThread(NetworkLocationProviderService::class.java.simpleName) + handlerThread.start() + handler = Handler(handlerThread.looper) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (Binder.getCallingUid() == Process.myUid() && intent?.action == ACTION_REPORT_LOCATION) { + handler.post { + val location = intent.getParcelableExtra(EXTRA_LOCATION) + if (location != null) { + provider?.reportLocation(location) + } + } + } + return START_NOT_STICKY + } + + override fun onBind(intent: Intent?): IBinder? { + bound = true + if (provider == null) { + provider = when { + // TODO: Migrate to Tiramisu provider. Not yet required thanks to backwards compat + // SDK_INT >= 33 -> + SDK_INT >= 31 -> + NetworkLocationProviderPreTiramisu(this) + + else -> + @Suppress("DEPRECATION") + NetworkLocationProviderPreTiramisu(this, Unit) + } + provider?.enable() + } + return provider?.getBinder() + } + + override fun dump(fd: FileDescriptor, writer: PrintWriter, args: Array) { + writer.println("Bound: $bound") + provider?.dump(writer) + } + + override fun onDestroy() { + if (SDK_INT >= 18) handlerThread.looper.quitSafely() + else handlerThread.looper.quit() + provider?.disable() + provider = null + bound = false + super.onDestroy() + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt new file mode 100644 index 0000000000..540e3b2565 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt @@ -0,0 +1,177 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.content.Context +import android.location.Address +import android.location.GeocoderParams +import android.net.Uri +import android.util.Log +import android.util.LruCache +import com.android.location.provider.GeocodeProvider +import com.android.volley.toolbox.JsonArrayRequest +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.google.android.gms.location.internal.ClientIdentity +import org.json.JSONObject +import org.microg.address.Formatter +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + + +class OpenStreetMapNominatimGeocodeProvider(private val context: Context) : GeocodeProvider() { + private val queue = Volley.newRequestQueue(context) + private val formatter = runCatching { Formatter() }.getOrNull() + private val addressCache = LruCache(CACHE_SIZE) + + override fun onGetFromLocation(latitude: Double, longitude: Double, maxResults: Int, params: GeocoderParams, addresses: MutableList
): String? { + val clientIdentity = params.clientIdentity ?: return "null client package" + val locale = params.locale ?: return "null locale" + val cacheKey = CacheKey(clientIdentity, locale, latitude, longitude) + addressCache[cacheKey]?.let {address -> + addresses.add(address) + return null + } + val uri = Uri.Builder() + .scheme("https").authority(NOMINATIM_SERVER).path("/reverse") + .appendQueryParameter("format", "json") + .appendQueryParameter("accept-language", locale.language) + .appendQueryParameter("addressdetails", "1") + .appendQueryParameter("lat", latitude.toString()) + .appendQueryParameter("lon", longitude.toString()) + val result = AtomicReference("timeout reached") + val returnedAddress = AtomicReference(null) + val latch = CountDownLatch(1) + queue.add(object : JsonObjectRequest(uri.build().toString(), { + parseResponse(locale, it)?.let(returnedAddress::set) + result.set(null) + latch.countDown() + }, { + result.set(it.message) + latch.countDown() + }) { + override fun getHeaders(): Map = mapOf("User-Agent" to "microG/${context.versionName}") + }) + latch.await(5, TimeUnit.SECONDS) + val address = returnedAddress.get() + if (address != null) { + Log.d(TAG, "Returned $address for $latitude,$longitude") + addresses.add(address) + addressCache.put(cacheKey, address) + } + return result.get() + } + + override fun onGetFromLocationName( + locationName: String, + lowerLeftLatitude: Double, + lowerLeftLongitude: Double, + upperRightLatitude: Double, + upperRightLongitude: Double, + maxResults: Int, + params: GeocoderParams, + addresses: MutableList
+ ): String? { + val clientIdentity = params.clientIdentity ?: return "null client package" + val locale = params.locale ?: return "null locale" + val uri = Uri.Builder() + .scheme("https").authority(NOMINATIM_SERVER).path("/search") + .appendQueryParameter("format", "json") + .appendQueryParameter("accept-language", locale.language) + .appendQueryParameter("addressdetails", "1") + .appendQueryParameter("bounded", "1") + .appendQueryParameter("q", locationName) + .appendQueryParameter("limit", maxResults.toString()) + if (lowerLeftLatitude != upperRightLatitude && lowerLeftLongitude != upperRightLongitude) { + uri.appendQueryParameter("viewbox", "$lowerLeftLongitude,$upperRightLatitude,$upperRightLongitude,$lowerLeftLatitude") + } + val result = AtomicReference("timeout reached") + val latch = CountDownLatch(1) + queue.add(object : JsonArrayRequest(uri.build().toString(), { + for (i in 0 until it.length()) { + parseResponse(locale, it.getJSONObject(i))?.let(addresses::add) + } + result.set(null) + latch.countDown() + }, { + result.set(it.message) + latch.countDown() + }) { + override fun getHeaders(): Map = mapOf("User-Agent" to "microG/${context.versionName}") + }) + latch.await(5, TimeUnit.SECONDS) + return result.get() + } + + private fun parseResponse(locale: Locale, result: JSONObject): Address? { + if (!result.has(WIRE_LATITUDE) || !result.has(WIRE_LONGITUDE) || + !result.has(WIRE_ADDRESS) + ) { + return null + } + Log.d(TAG, "Result: $result") + val address = Address(locale) + address.latitude = result.getDouble(WIRE_LATITUDE) + address.longitude = result.getDouble(WIRE_LONGITUDE) + val a = result.getJSONObject(WIRE_ADDRESS) + address.thoroughfare = a.optString(WIRE_THOROUGHFARE) + address.subLocality = a.optString(WIRE_SUBLOCALITY) + address.postalCode = a.optString(WIRE_POSTALCODE) + address.subAdminArea = a.optString(WIRE_SUBADMINAREA) + address.adminArea = a.optString(WIRE_ADMINAREA) + address.countryName = a.optString(WIRE_COUNTRYNAME) + address.countryCode = a.optString(WIRE_COUNTRYCODE) + if (a.has(WIRE_LOCALITY_CITY)) { + address.locality = a.getString(WIRE_LOCALITY_CITY) + } else if (a.has(WIRE_LOCALITY_TOWN)) { + address.locality = a.getString(WIRE_LOCALITY_TOWN) + } else if (a.has(WIRE_LOCALITY_VILLAGE)) { + address.locality = a.getString(WIRE_LOCALITY_VILLAGE) + } + if (formatter != null) { + val components = mutableMapOf() + for (s in a.keys()) { + if (s !in WIRE_IGNORED) { + components[s] = a[s].toString() + } + } + val split = formatter.formatAddress(components).split("\n") + for (i in split.indices) { + address.setAddressLine(i, split[i]) + } + address.featureName = formatter.guessName(components) + } + return address + } + + companion object { + private const val CACHE_SIZE = 200 + + private const val NOMINATIM_SERVER = "nominatim.openstreetmap.org" + + private const val WIRE_LATITUDE = "lat" + private const val WIRE_LONGITUDE = "lon" + private const val WIRE_ADDRESS = "address" + private const val WIRE_THOROUGHFARE = "road" + private const val WIRE_SUBLOCALITY = "suburb" + private const val WIRE_POSTALCODE = "postcode" + private const val WIRE_LOCALITY_CITY = "city" + private const val WIRE_LOCALITY_TOWN = "town" + private const val WIRE_LOCALITY_VILLAGE = "village" + private const val WIRE_SUBADMINAREA = "county" + private const val WIRE_ADMINAREA = "state" + private const val WIRE_COUNTRYNAME = "country" + private const val WIRE_COUNTRYCODE = "country_code" + + private val WIRE_IGNORED = setOf("ISO3166-2-lvl4") + + private data class CacheKey(val uid: Int, val packageName: String?, val locale: Locale, val latitude: Int, val longitude: Int) { + constructor(clientIdentity: ClientIdentity, locale: Locale, latitude: Double, longitude: Double) : this(clientIdentity.uid, clientIdentity.packageName.takeIf { clientIdentity.uid != 0 }, locale, (latitude * 100000.0).toInt(), (longitude * 100000.0).toInt()) + } + } +} \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/extensions.kt new file mode 100644 index 0000000000..eea68e661a --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/provider/extensions.kt @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.content.Context +import android.content.pm.PackageManager +import android.location.GeocoderParams +import android.os.Build.VERSION.SDK_INT +import com.google.android.gms.location.internal.ClientIdentity +import org.microg.gms.utils.getApplicationLabel + +const val TAG = "LocationProvider" + +val GeocoderParams.clientIdentity: ClientIdentity? + get() = clientPackage?.let { + ClientIdentity(it).apply { + if (SDK_INT >= 33) { + uid = clientUid + attributionTag = clientAttributionTag + } + } + } + +val Context.versionName: String + get() = packageManager.getPackageInfo(packageName, 0).versionName \ No newline at end of file diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingAndroidService.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingAndroidService.kt new file mode 100644 index 0000000000..68f260875b --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingAndroidService.kt @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.gms.location.reporting + +import android.os.RemoteException +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.location.FEATURES + +class ReportingAndroidService : BaseService("GmsLocReportingSvc", GmsService.LOCATION_REPORTING) { + @Throws(RemoteException::class) + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + ReportingServiceInstance(this, packageName), + ConnectionInfo().apply { features = FEATURES } + ) + } +} diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingServiceInstance.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingServiceInstance.kt new file mode 100644 index 0000000000..df633590da --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/ReportingServiceInstance.kt @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.gms.location.reporting + +import android.accounts.Account +import android.content.Context +import android.os.Parcel +import android.util.Log +import com.google.android.gms.location.reporting.* +import com.google.android.gms.location.reporting.internal.IReportingService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues + +//import com.google.android.gms.location.places.PlaceReport; +class ReportingServiceInstance(private val context: Context, private val packageName: String) : IReportingService.Stub() { + + override fun getReportingState(account: Account): ReportingState { + Log.d(TAG, "getReportingState") + val state = ReportingState() + if (PackageUtils.callerHasExtendedAccess(context)) { + state.deviceTag = 0 + } + return state + } + + override fun tryOptInAccount(account: Account): Int { + val request = OptInRequest() + request.account = account + return tryOptIn(request) + } + + override fun requestUpload(request: UploadRequest): UploadRequestResult { + Log.d(TAG, "requestUpload") + return UploadRequestResult() + } + + override fun cancelUploadRequest(l: Long): Int { + Log.d(TAG, "cancelUploadRequest") + return 0 + } + + // @Override + // public int reportDeviceAtPlace(Account account, PlaceReport report) throws RemoteException { + // Log.d(TAG, "reportDeviceAtPlace"); + // return 0; + // } + + override fun tryOptIn(request: OptInRequest): Int { + return 0 + } + + override fun sendData(request: SendDataRequest): Int { + return 0 + } + + override fun requestPrivateMode(request: UlrPrivateModeRequest): Int { + return 0 + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } +} diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/extensions.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/extensions.kt new file mode 100644 index 0000000000..5da8000099 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/reporting/extensions.kt @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.reporting + +const val TAG = "LocationReporting" \ No newline at end of file diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/ActivityRecognitionRequest.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/ActivityRecognitionRequest.aidl new file mode 100644 index 0000000000..a7104175c2 --- /dev/null +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/ActivityRecognitionRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.location; + +parcelable ActivityRecognitionRequest; \ No newline at end of file diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/ActivityTransitionRequest.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/ActivityTransitionRequest.aidl new file mode 100644 index 0000000000..e7f5b54539 --- /dev/null +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/ActivityTransitionRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.location; + +parcelable ActivityTransitionRequest; \ No newline at end of file diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/CurrentLocationRequest.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/CurrentLocationRequest.aidl new file mode 100644 index 0000000000..cab0deea69 --- /dev/null +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/CurrentLocationRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.location; + +parcelable CurrentLocationRequest; diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/IDeviceOrientationListener.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/IDeviceOrientationListener.aidl index e3d3e6f0ea..0a6684ff5e 100644 --- a/play-services-location/src/main/aidl/com/google/android/gms/location/IDeviceOrientationListener.aidl +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/IDeviceOrientationListener.aidl @@ -3,5 +3,5 @@ package com.google.android.gms.location; import com.google.android.gms.location.DeviceOrientation; interface IDeviceOrientationListener { - void onDeviceOrientationChanged(in DeviceOrientation deviceOrientation); + oneway void onDeviceOrientationChanged(in DeviceOrientation deviceOrientation); } diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/ILocationCallback.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/ILocationCallback.aidl index bbccd355da..621a2aa972 100644 --- a/play-services-location/src/main/aidl/com/google/android/gms/location/ILocationCallback.aidl +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/ILocationCallback.aidl @@ -6,6 +6,7 @@ import com.google.android.gms.location.LocationAvailability; import com.google.android.gms.location.LocationResult; interface ILocationCallback { - void onLocationResult(in LocationResult result) = 0; - void onLocationAvailability(in LocationAvailability availability) = 1; + oneway void onLocationResult(in LocationResult result) = 0; + oneway void onLocationAvailability(in LocationAvailability availability) = 1; + oneway void cancel() = 2; } diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/ILocationListener.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/ILocationListener.aidl index 323f9059bf..87385315e5 100644 --- a/play-services-location/src/main/aidl/com/google/android/gms/location/ILocationListener.aidl +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/ILocationListener.aidl @@ -3,5 +3,6 @@ package com.google.android.gms.location; import android.location.Location; interface ILocationListener { - void onLocationChanged(in Location location); + oneway void onLocationChanged(in Location location) = 0; + oneway void cancel() = 1; } diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/LastLocationRequest.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/LastLocationRequest.aidl new file mode 100644 index 0000000000..0f562aead1 --- /dev/null +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/LastLocationRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.location; + +parcelable LastLocationRequest; diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/LocationAvailabilityRequest.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/LocationAvailabilityRequest.aidl new file mode 100644 index 0000000000..64309ec95c --- /dev/null +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/LocationAvailabilityRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.location; + +parcelable LocationAvailabilityRequest; diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/SleepSegmentRequest.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/SleepSegmentRequest.aidl new file mode 100644 index 0000000000..5891a7ea32 --- /dev/null +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/SleepSegmentRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.location; + +parcelable SleepSegmentRequest; diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IFusedLocationProviderCallback.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IFusedLocationProviderCallback.aidl index 44f0d67baf..cce306b69c 100644 --- a/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IFusedLocationProviderCallback.aidl +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IFusedLocationProviderCallback.aidl @@ -4,4 +4,5 @@ import com.google.android.gms.location.internal.FusedLocationProviderResult; interface IFusedLocationProviderCallback { oneway void onFusedLocationProviderResult(in FusedLocationProviderResult result) = 0; + oneway void cancel() = 1; } diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGeofencerCallbacks.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGeofencerCallbacks.aidl index ac3950f575..a89f6153e3 100644 --- a/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGeofencerCallbacks.aidl +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGeofencerCallbacks.aidl @@ -3,7 +3,7 @@ package com.google.android.gms.location.internal; import android.app.PendingIntent; interface IGeofencerCallbacks { - void onAddGeofenceResult(int statusCode, in String[] requestIds) = 0; - void onRemoveGeofencesByRequestIdsResult(int statusCode, in String[] requestIds) = 1; - void onRemoveGeofencesByPendingIntentResult(int statusCode, in PendingIntent pendingIntent) = 2; + oneway void onAddGeofenceResult(int statusCode, in String[] requestIds) = 0; + oneway void onRemoveGeofencesByRequestIdsResult(int statusCode, in String[] requestIds) = 1; + oneway void onRemoveGeofencesByPendingIntentResult(int statusCode, in PendingIntent pendingIntent) = 2; } diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGoogleLocationManagerService.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGoogleLocationManagerService.aidl index 6df446c780..132bad1c1e 100644 --- a/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGoogleLocationManagerService.aidl +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGoogleLocationManagerService.aidl @@ -6,79 +6,92 @@ import android.os.Bundle; import com.google.android.gms.common.api.Status; import com.google.android.gms.common.api.internal.IStatusCallback; -//import com.google.android.gms.location.places.AutocompleteFilter; -//import com.google.android.gms.location.places.internal.IPlacesCallbacks; +import com.google.android.gms.common.internal.ICancelToken; import com.google.android.gms.location.internal.DeviceOrientationRequestUpdateData; import com.google.android.gms.location.internal.IFusedLocationProviderCallback; +import com.google.android.gms.location.internal.IGeofencerCallbacks; +import com.google.android.gms.location.internal.ILocationStatusCallback; import com.google.android.gms.location.internal.ISettingsCallbacks; +import com.google.android.gms.location.internal.LocationReceiver; import com.google.android.gms.location.internal.LocationRequestInternal; import com.google.android.gms.location.internal.LocationRequestUpdateData; import com.google.android.gms.location.internal.ParcelableGeofence; -//import com.google.android.gms.location.places.NearbyAlertRequest; -//import com.google.android.gms.location.places.PlaceFilter; -//import com.google.android.gms.location.places.PlaceRequest; -//import com.google.android.gms.location.places.PlaceReport; -//import com.google.android.gms.location.places.internal.PlacesParams; -//import com.google.android.gms.location.places.UserAddedPlace; -//import com.google.android.gms.location.places.UserDataType; import com.google.android.gms.location.ActivityRecognitionResult; +import com.google.android.gms.location.ActivityRecognitionRequest; +import com.google.android.gms.location.ActivityTransitionRequest; +import com.google.android.gms.location.CurrentLocationRequest; import com.google.android.gms.location.GeofencingRequest; import com.google.android.gms.location.GestureRequest; +import com.google.android.gms.location.ILocationListener; +import com.google.android.gms.location.LastLocationRequest; import com.google.android.gms.location.LocationAvailability; +import com.google.android.gms.location.LocationAvailabilityRequest; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationSettingsRequest; import com.google.android.gms.location.LocationStatus; -import com.google.android.gms.location.internal.IGeofencerCallbacks; -import com.google.android.gms.location.ILocationListener; -//import com.google.android.gms.maps.model.LatLng; -//import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.location.SleepSegmentRequest; interface IGoogleLocationManagerService { - void addGeofencesList(in List geofences, in PendingIntent pendingIntent, IGeofencerCallbacks callbacks, String packageName) = 0; + + // current + void addGeofences(in GeofencingRequest geofencingRequest, in PendingIntent pendingIntent, IGeofencerCallbacks callbacks) = 56; void removeGeofencesByIntent(in PendingIntent pendingIntent, IGeofencerCallbacks callbacks, String packageName) = 1; void removeGeofencesById(in String[] geofenceRequestIds, IGeofencerCallbacks callbacks, String packageName) = 2; - void removeAllGeofences(IGeofencerCallbacks callbacks, String packageName) = 3; -// void removeGeofences(in RemoveGeofencingRequest request, IGeofencerCallbacks callback) = 73; - void requestActivityUpdates(long detectionIntervalMillis, boolean alwaysTrue, in PendingIntent callbackIntent) = 4; + void removeActivityTransitionUpdates(in PendingIntent pendingIntent, IStatusCallback callback) = 72; void removeActivityUpdates(in PendingIntent callbackIntent) = 5; - ActivityRecognitionResult getLastActivity(String packageName) = 63; - - Status requestGestureUpdates(in GestureRequest request, in PendingIntent pendingIntent) = 59; - Status iglms61(in PendingIntent pendingIntent) = 60; + void requestActivityTransitionUpdates(in ActivityTransitionRequest request, in PendingIntent pendingIntent, IStatusCallback callback) = 71; + void requestActivityUpdates(long detectionIntervalMillis, boolean triggerUpdates, in PendingIntent callbackIntent) = 4; + void requestActivityUpdatesWithCallback(in ActivityRecognitionRequest request, in PendingIntent pendingIntent, IStatusCallback callback) = 69; Location getLastLocation() = 6; - Location getLastLocationWithPackage(String packageName) = 20; - Location getLastLocationWith(String s) = 79; + void getLastLocationWithRequest(in LastLocationRequest request, ILocationStatusCallback callback) = 81; + void getLastLocationWithReceiver(in LastLocationRequest request, in LocationReceiver receiver) = 89; - void requestLocationUpdatesWithListener(in LocationRequest request, ILocationListener listener) = 7; - void requestLocationUpdatesWithPackage(in LocationRequest request, ILocationListener listener, String packageName) = 19; - void requestLocationUpdatesWithIntent(in LocationRequest request, in PendingIntent callbackIntent) = 8; - void requestLocationUpdatesInternalWithListener(in LocationRequestInternal request, ILocationListener listener) = 51; - void requestLocationUpdatesInternalWithIntent(in LocationRequestInternal request, in PendingIntent callbackIntent) = 52; - void removeLocationUpdatesWithListener(ILocationListener listener) = 9; - void removeLocationUpdatesWithIntent(in PendingIntent callbackIntent) = 10; + ICancelToken getCurrentLocation(in CurrentLocationRequest request, ILocationStatusCallback callback) = 86; + ICancelToken getCurrentLocationWithReceiver(in CurrentLocationRequest request, in LocationReceiver receiver) = 91; + + void requestLocationUpdatesWithCallback(in LocationReceiver receiver, in LocationRequest request, IStatusCallback callback) = 87; + void removeLocationUpdatesWithCallback(in LocationReceiver receiver, IStatusCallback callback) = 88; void updateLocationRequest(in LocationRequestUpdateData locationRequestUpdateData) = 58; + void flushLocations(IFusedLocationProviderCallback callback) = 66; + LocationAvailability getLocationAvailabilityWithPackage(String packageName) = 33; + void getLocationAvailabilityWithReceiver(in LocationAvailabilityRequest request, in LocationReceiver receiver) = 90; + void setMockMode(boolean mockMode) = 11; void setMockLocation(in Location mockLocation) = 12; - void injectLocation(in Location mockLocation, int injectionType) = 25; + void setMockModeWithCallback(boolean mockMode, IStatusCallback callback) = 83; + void setMockLocationWithCallback(in Location mockLocation, IStatusCallback callback) = 84; - LocationAvailability getLocationAvailabilityWithPackage(String packageName) = 33; - -// void requestSleepSegmentUpdates(in PendingIntent pendingIntent, in SleepSegmentRequest request, IStatusCallback callback) = 78; void removeSleepSegmentUpdates(in PendingIntent pendingIntent, IStatusCallback callback) = 68; + void requestSleepSegmentUpdates(in PendingIntent pendingIntent, in SleepSegmentRequest request, IStatusCallback callback) = 78; void requestLocationSettingsDialog(in LocationSettingsRequest settingsRequest, ISettingsCallbacks callback, String packageName) = 62; -// void requestActivityTransitionUpdates(in ActivityTransitionRequest request, in PendingIntent pendingIntent, IStatusCallback callback) = 71; - void removeActivityTransitionUpdates(in PendingIntent pendingIntent, IStatusCallback callback) = 72; - void updateDeviceOrientationRequest(in DeviceOrientationRequestUpdateData request) = 74; - boolean setActivityRecognitionMode(int mode) = 76; + // deprecated + + void addGeofencesList(in List geofences, in PendingIntent pendingIntent, IGeofencerCallbacks callbacks, String packageName) = 0; + void removeAllGeofences(IGeofencerCallbacks callbacks, String packageName) = 3; + + ActivityRecognitionResult getLastActivity(String packageName) = 63; + + Location getLastLocationWithPackage(String packageName) = 20; + Location getLastLocationWith(String s) = 79; + + void requestLocationUpdatesWithListener(in LocationRequest request, ILocationListener listener) = 7; + void requestLocationUpdatesWithPackage(in LocationRequest request, ILocationListener listener, String packageName) = 19; + void requestLocationUpdatesWithIntent(in LocationRequest request, in PendingIntent callbackIntent) = 8; + void requestLocationUpdatesInternalWithListener(in LocationRequestInternal request, ILocationListener listener) = 51; + void requestLocationUpdatesInternalWithIntent(in LocationRequestInternal request, in PendingIntent callbackIntent) = 52; + void removeLocationUpdatesWithListener(ILocationListener listener) = 9; + void removeLocationUpdatesWithIntent(in PendingIntent callbackIntent) = 10; + + // unsupported // void iglms14(in LatLngBounds var1, int var2, in PlaceFilter var3, in PlacesParams var4, IPlacesCallbacks var5) = 13; // void iglms15(String var1, in PlacesParams var2, IPlacesCallbacks var3) = 14; @@ -88,6 +101,7 @@ interface IGoogleLocationManagerService { // void iglms19(in PlacesParams var1, in PendingIntent var2) = 18; // void iglms25(in PlaceReport var1, in PlacesParams var2) = 24; +// void injectLocation(in Location mockLocation, int injectionType) = 25; // void iglms42(String var1, in PlacesParams var2, IPlacesCallbacks var3) = 41; @@ -96,19 +110,27 @@ interface IGoogleLocationManagerService { // void iglms48(in NearbyAlertRequest var1, in PlacesParams var2, in PendingIntent var3) = 47; // void iglms49(in PlacesParams var1, in PendingIntent var2) = 48; // void iglms50(in UserDataType var1, in LatLngBounds var2, in List var3, in PlacesParams var4, IPlacesCallbacks var5) = 49; - IBinder iglms51() = 50; +// IBinder iglms51() = 50; - IBinder iglms54() = 53; +// IBinder iglms54() = 53; // void iglms55(String var1, in LatLngBounds var2, in AutocompleteFilter var3, in PlacesParams var4, IPlacesCallbacks var5) = 54; // void iglms58(in List var1, in PlacesParams var2, IPlacesCallbacks var3) = 57; - void iglms65(in PendingIntent pendingIntent, IStatusCallback callback) = 64; - void iglms66(in PendingIntent pendingIntent, IStatusCallback callback) = 65; +// Status requestGestureUpdates(in GestureRequest request, in PendingIntent pendingIntent) = 59; +// Status iglms61(in PendingIntent pendingIntent) = 60; + +// void iglms65(in PendingIntent pendingIntent, IStatusCallback callback) = 64; +// void iglms66(in PendingIntent pendingIntent, IStatusCallback callback) = 65; + +// void iglms68(in PendingIntent pendingIntent, IStatusCallback callback) = 67; + +// void iglms71(IStatusCallback callback) = 70; + +// void removeGeofences(in RemoveGeofencingRequest request, IGeofencerCallbacks callback) = 73; +// void iglms76(in PendingIntent pendingIntent) = 75; +// boolean setActivityRecognitionMode(int mode) = 76; +// int getActivityRecognitionMode() = 77; - void iglms68(in PendingIntent pendingIntent, IStatusCallback callback) = 67; -// void iglms70(in ActivityRecognitionRequest request, in PendingIntent pendingIntent, IStatusCallback callback) = 69; - void iglms71(IStatusCallback callback) = 70; - void iglms76(in PendingIntent pendingIntent) = 75; - int iglms78() = 77; +// void injectLocatinWithCallback(in Location mockLocation, int injectionType, IStatusCallback callback) = 85; } diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/internal/ILocationAvailabilityStatusCallback.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/ILocationAvailabilityStatusCallback.aidl new file mode 100644 index 0000000000..39f907c4e5 --- /dev/null +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/ILocationAvailabilityStatusCallback.aidl @@ -0,0 +1,8 @@ +package com.google.android.gms.location.internal; + +import com.google.android.gms.common.api.Status; +import com.google.android.gms.location.LocationAvailability; + +interface ILocationAvailabilityStatusCallback { + void onLocationAvailabilityStatus(in Status status, in LocationAvailability location); +} diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/internal/ILocationStatusCallback.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/ILocationStatusCallback.aidl new file mode 100644 index 0000000000..4bea77da0d --- /dev/null +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/ILocationStatusCallback.aidl @@ -0,0 +1,8 @@ +package com.google.android.gms.location.internal; + +import android.location.Location; +import com.google.android.gms.common.api.Status; + +interface ILocationStatusCallback { + void onLocationStatus(in Status status, in Location location); +} diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/internal/LocationReceiver.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/LocationReceiver.aidl new file mode 100644 index 0000000000..bf7b6d74a6 --- /dev/null +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/LocationReceiver.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.location.internal; + +parcelable LocationReceiver; diff --git a/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognition.java b/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognition.java index 10e8a29844..7bebd7bb7d 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognition.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognition.java @@ -30,6 +30,7 @@ public class ActivityRecognition { * * @deprecated Use {@link ActivityRecognitionClient} instead. */ + @Deprecated public static final Api API = new Api(new ActivityRecognitionApiClientBuilder()); /** @@ -37,6 +38,7 @@ public class ActivityRecognition { * * @deprecated Use {@link ActivityRecognitionClient} instead. */ + @Deprecated public static final ActivityRecognitionApi ActivityRecognitionApi = new ActivityRecognitionApiImpl(); /** diff --git a/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionClient.java b/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionClient.java index aaa8ddd0ee..21824cf8ec 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionClient.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionClient.java @@ -64,24 +64,42 @@ public interface ActivityRecognitionClient extends HasApiKey removeSleepSegmentUpdates(PendingIntent callbackIntent); /** - * @param activityTransitionRequest - * @param pendingIntent + * Activity Recognition Transition API provides an ability for apps to subscribe to activity transitional conditions (enter, exit). For example, a + * messaging app that wants to build a distraction free driving experiences can ask -- tell me when user has entered the vehicle or exited the vehicle. It + * doesn't have to worry about user being {@link DetectedActivity#STILL} at the traffic signal, or any other transient activities while in vehicle + * ({@link DetectedActivity#IN_VEHICLE}), that is, the API will fence around the activity boundaries using Activity Recognition Filtering. + * + * @param activityTransitionRequest the interested activity transitions + * @param pendingIntent the {@link PendingIntent} used to generate the callback intent when one of the interested transition has happened * @return a {@link Task} for apps to check the status of the call. If the task fails, the status code for the * failure can be found by examining {@link ApiException#getStatusCode()}. */ Task requestActivityTransitionUpdates(ActivityTransitionRequest activityTransitionRequest, PendingIntent pendingIntent); /** - * @param detectionIntervalMillis - * @param callbackIntent + * Register for activity recognition updates. + *

+ * The activities are detected by periodically waking up the device and reading short bursts of sensor data. It only makes use of low power sensors in order + * to keep the power usage to a minimum. For example, it can detect if the user is currently on foot, in a car, on a bicycle or still. See + * {@link DetectedActivity} for more details. + * + * @param detectionIntervalMillis the desired time between activity detections. Larger values will result in fewer activity detections while improving + * battery life. A value of 0 will result in activity detections at the fastest possible rate. Note that a fast rate can + * result in excessive device wakelocks and power consumption. + * @param callbackIntent a PendingIntent to be sent for each activity detection. * @return a {@link Task} for apps to check the status of the call. If the task fails, the status code for the * failure can be found by examining {@link ApiException#getStatusCode()}. */ Task requestActivityUpdates(long detectionIntervalMillis, PendingIntent callbackIntent); /** - * @param callbackIntent - * @param sleepSegmentRequest + * Registers for detected user sleep time ({@code SleepSegmentEvent}) and/or periodic sleep activity classification results ({@code SleepClassifyEvent}) + * based on the data type specified in {@link SleepSegmentRequest}. It is advised to the apps to re-register after device reboot or app upgrade, from a + * receiver that handles {@code android.intent.action.BOOT_COMPLETED} and {@code android.intent.action.MY_PACKAGE_REPLACED} events. + * + * @param callbackIntent a PendingIntent to be sent for each sleep segment or classification result + * @param sleepSegmentRequest a {@link SleepSegmentRequest} that specifies whether to receive both {@code SleepSegmentEvent}s and + * {@code SleepClassifyEvent}s, or {@code SleepSegmentEvent}s only, or {@code SleepClassifyEvent}s only. * @return a {@link Task} for apps to check the status of the call. If the task fails, the status code for the * failure can be found by examining {@link ApiException#getStatusCode()}. */ diff --git a/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionRequest.java b/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionRequest.java new file mode 100644 index 0000000000..91353f0776 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionRequest.java @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.location; + +import android.os.WorkSource; +import androidx.annotation.Nullable; +import org.microg.gms.common.Hide; +import org.microg.safeparcel.AutoSafeParcelable; + +@Hide +public class ActivityRecognitionRequest extends AutoSafeParcelable { + @Field(1) + public long intervalMillis; + @Field(2) + public boolean triggerUpdate; + @Field(3) + @Nullable + public WorkSource workSource; + @Field(4) + @Nullable + public String tag; + @Field(5) + @Nullable + public int[] nonDefaultActivities; + @Field(6) + public boolean requestSensorData; + @Field(7) + @Nullable + public String accountName; + @Field(8) + public long maxReportLatencyMillis; + @Field(9) + @Nullable + public String contextAttributionTag; + + public static final Creator CREATOR = new AutoCreator<>(ActivityRecognitionRequest.class); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransition.java b/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransition.java new file mode 100644 index 0000000000..ac1672dff9 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransition.java @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import org.microg.safeparcel.AutoSafeParcelable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Objects; + +/** + * Represents an activity and the transition of it. For instance start to walk; stop running etc. + */ +public class ActivityTransition extends AutoSafeParcelable { + /** + * User enters the given activity. + */ + public static final int ACTIVITY_TRANSITION_ENTER = 0; + /** + * User exits the given activity. + */ + public static final int ACTIVITY_TRANSITION_EXIT = 1; + + @Field(1) + private int activityType; + @Field(2) + private @SupportedActivityTransition int transitionType; + + private ActivityTransition() { + } + + private ActivityTransition(int activityType, @SupportedActivityTransition int transitionType) { + this.activityType = activityType; + this.transitionType = transitionType; + } + + /** + * Gets the type of the activity to be detected. + */ + public int getActivityType() { + return activityType; + } + + /** + * Gets the interested transition type. It's one of the ACTIVITY_TRANSITION_xxx constants. + */ + public @SupportedActivityTransition int getTransitionType() { + return transitionType; + } + + @Override + public int hashCode() { + return Objects.hashCode(new Object[]{activityType, transitionType}); + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof ActivityTransition && ((ActivityTransition) obj).activityType == activityType && ((ActivityTransition) obj).transitionType == transitionType; + } + + @NonNull + @Override + public String toString() { + return "ActivityTransition [mActivityType=" + activityType + ", mTransitionType=" + transitionType + "]"; + } + + /** + * Activity transition constants annotation. + */ + @Target({ElementType.TYPE_USE}) + @Retention(RetentionPolicy.SOURCE) + @IntDef({ACTIVITY_TRANSITION_ENTER, ACTIVITY_TRANSITION_EXIT}) + public @interface SupportedActivityTransition { + } + + /** + * The builder to help create an {@link ActivityTransition} object. + */ + public static class Builder { + private int activityType; + private int transitionType; + + /** + * Adds an interested transition type. + * + * @param transition the interested transition type. It's one of the ACTIVITY_TRANSITION_xxx constants. + * @return this builder + */ + public ActivityTransition.Builder setActivityTransition(int transition) { + this.transitionType = transition; + return this; + } + + /** + * Sets the type of the activity to be detected. + * + * @param activityType the type of the activity to be detected. It's one of the constant in {@link DetectedActivity}. + * @return this builder + */ + public ActivityTransition.Builder setActivityType(int activityType) { + this.activityType = activityType; + return this; + } + + public ActivityTransition build() { + return new ActivityTransition(activityType, transitionType); + } + } + + public static final Creator CREATOR = new AutoCreator<>(ActivityTransition.class); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionEvent.java b/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionEvent.java new file mode 100644 index 0000000000..365c06c3a0 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionEvent.java @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import androidx.annotation.NonNull; +import org.microg.gms.common.PublicApi; +import org.microg.safeparcel.AutoSafeParcelable; + +import java.util.Objects; + +/** + * Represents an activity transition event, for example start to walk, stop running etc. + */ +@PublicApi +public class ActivityTransitionEvent extends AutoSafeParcelable { + @Field(1) + private int activityType; + @Field(2) + private int transitionType; + @Field(3) + private long elapsedRealtimeNanos; + + private ActivityTransitionEvent() { + } + + /** + * Creates an activity transition event. + * + * @param activityType the type of the activity of this transition + * @param transitionType the type of transition + * @param elapsedRealtimeNanos the elapsed realtime when this transition happened + */ + public ActivityTransitionEvent(int activityType, int transitionType, long elapsedRealtimeNanos) { + this.activityType = activityType; + this.transitionType = transitionType; + this.elapsedRealtimeNanos = elapsedRealtimeNanos; + } + + /** + * Gets the type of the activity of this transition. It's one of activity types defined in {@link DetectedActivity}. + */ + public int getActivityType() { + return activityType; + } + + /** + * Gets the elapsed realtime when this transition happened. Note that the event may happen in the past which means this timestamp may be much smaller than + * the current time. + */ + public long getElapsedRealTimeNanos() { + return elapsedRealtimeNanos; + } + + /** + * Gets the type of the transition. It's one of the transition types defined in {@link ActivityTransition}. + */ + public int getTransitionType() { + return transitionType; + } + + @Override + public int hashCode() { + return Objects.hashCode(new Object[]{activityType, transitionType, elapsedRealtimeNanos}); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ActivityTransitionEvent)) return false; + + ActivityTransitionEvent that = (ActivityTransitionEvent) o; + + if (activityType != that.activityType) return false; + if (transitionType != that.transitionType) return false; + return elapsedRealtimeNanos == that.elapsedRealtimeNanos; + } + + @NonNull + @Override + public String toString() { + return "ActivityType " + activityType + " TransitionType " + transitionType + " ElapsedRealTimeNanos " + elapsedRealtimeNanos; + } + + public static final Creator CREATOR = new AutoCreator<>(ActivityTransitionEvent.class); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionRequest.java b/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionRequest.java index 781050ef77..33947e785a 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionRequest.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionRequest.java @@ -8,11 +8,100 @@ package com.google.android.gms.location; +import android.content.Intent; +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.ClientIdentity; +import org.microg.gms.common.PublicApi; import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParcelUtil; + +import java.util.*; /** * The request object for apps to get notified when user's activity changes. */ +@PublicApi public class ActivityTransitionRequest extends AutoSafeParcelable { + private static final String EXTRA = "com.google.android.location.internal.EXTRA_ACTIVITY_TRANSITION_REQUEST"; + + @Field(value = 1, subClass = ActivityTransition.class) + private List activityTransitions; + @Field(2) + private String tag; + @Field(value = 3, subClass = ClientIdentity.class) + private List clients; + @Field(4) + private String contextAttributionTag; + + /** + * The comparator used to determine if two transitions are the same. It's different from {@link ActivityTransition#equals(Object)} because in the future we + * may add latency to activity transition and the latency value should not be compared against. + */ + public static final Comparator IS_SAME_TRANSITION = new Comparator() { + @Override + public int compare(ActivityTransition o1, ActivityTransition o2) { + int res = Integer.compare(o1.getActivityType(), o2.getActivityType()); + if (res != 0) return res; + res = Integer.compare(o1.getTransitionType(), o2.getTransitionType()); + return res; + } + }; + + private ActivityTransitionRequest() { + } + + /** + * Creates an {@link ActivityTransitionRequest} object by specifying a list of interested activity transitions. + * + * @param transitions a list of interested activity transitions + * @throws NullPointerException if {@code transitions} is {@code null} + * @throws IllegalArgumentException if {@code transitions} is an empty list or if there are duplicated transitions in this list + */ + public ActivityTransitionRequest(List transitions) { + if (transitions == null) throw new NullPointerException("transitions can't be null"); + if (transitions.isEmpty()) throw new IllegalArgumentException("transitions can't be empty."); + Set set = new TreeSet(IS_SAME_TRANSITION); + set.addAll(transitions); + if (transitions.size() != set.size()) throw new IllegalArgumentException("Found duplicated transition"); + this.activityTransitions = Collections.unmodifiableList(transitions); + } + + /** + * Serializes this request to the given intent. + * + * @param intent the intent to serailize this object to + */ + public void serializeToIntentExtra(Intent intent) { + intent.putExtra(EXTRA, SafeParcelUtil.asByteArray(this)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ActivityTransitionRequest that = (ActivityTransitionRequest) o; + + if (!Objects.equals(activityTransitions, that.activityTransitions)) return false; + if (!Objects.equals(tag, that.tag)) return false; + if (!Objects.equals(clients, that.clients)) return false; + return Objects.equals(contextAttributionTag, that.contextAttributionTag); + } + + @Override + public int hashCode() { + int result = activityTransitions != null ? activityTransitions.hashCode() : 0; + result = 31 * result + (tag != null ? tag.hashCode() : 0); + result = 31 * result + (clients != null ? clients.hashCode() : 0); + result = 31 * result + (contextAttributionTag != null ? contextAttributionTag.hashCode() : 0); + return result; + } + + @NonNull + @Override + public String toString() { + return "ActivityTransitionRequest [mTransitions=" + activityTransitions + ", mTag=" + tag + ", mClients" + clients + ", mAttributionTag=" + contextAttributionTag + "]"; + } + public static final Creator CREATOR = new AutoCreator<>(ActivityTransitionRequest.class); } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionResult.java b/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionResult.java new file mode 100644 index 0000000000..3abe47294d --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/ActivityTransitionResult.java @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.NonNull; +import org.microg.gms.common.PublicApi; +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParcelUtil; + +import java.util.Collections; +import java.util.List; + +/** + * Represents the result of activity transitions. + */ +@PublicApi +public class ActivityTransitionResult extends AutoSafeParcelable { + private static final String EXTRA = "com.google.android.location.internal.EXTRA_ACTIVITY_TRANSITION_RESULT"; + + @Field(value = 1, subClass = ActivityTransitionEvent.class) + @NonNull + private List transitionEvents; + @Field(2) + private Bundle extras; + + /** + * Constructs a result by specifying a list of transition events. + * + * @param transitionEvents the transition events + * @throws NullPointerException if {@code transitionEvents} is {@code null} + * @throws IllegalArgumentException if the events in {@code transitionEvents} are not in ascending order of time + */ + public ActivityTransitionResult(List transitionEvents) { + if (transitionEvents == null) throw new NullPointerException("transitionEvents list can't be null."); + for (int i = 1; i < transitionEvents.size(); i++) { + if (transitionEvents.get(i).getElapsedRealTimeNanos() < transitionEvents.get(i - 1).getElapsedRealTimeNanos()) + throw new IllegalArgumentException(); + } + this.transitionEvents = Collections.unmodifiableList(transitionEvents); + } + + /** + * Gets all the activity transition events in this result. The events are in ascending order of time, and may include events in the past. + */ + public List getTransitionEvents() { + return transitionEvents; + } + + /** + * Extracts the {@link ActivityTransitionResult} from the given {@link Intent}. + * + * @param intent the {@link Intent} to extract the result from + * @return the {@link ActivityTransitionResult} included in the given intent or return {@code null} if no such result is found in the given intent + */ + public static ActivityTransitionResult extractResult(Intent intent) { + if (!hasResult(intent)) return null; + return SafeParcelUtil.fromByteArray(intent.getByteArrayExtra(EXTRA), CREATOR); + } + + /** + * Checks if the intent contains an {@link ActivityTransitionResult}. + */ + public static boolean hasResult(Intent intent) { + return intent != null && intent.hasExtra(EXTRA); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ActivityTransitionResult that = (ActivityTransitionResult) o; + + return transitionEvents.equals(that.transitionEvents); + } + + @Override + public int hashCode() { + return transitionEvents.hashCode(); + } + + public static final Creator CREATOR = new AutoCreator<>(ActivityTransitionResult.class); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/CurrentLocationRequest.java b/play-services-location/src/main/java/com/google/android/gms/location/CurrentLocationRequest.java new file mode 100644 index 0000000000..b0103e7541 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/CurrentLocationRequest.java @@ -0,0 +1,269 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import android.os.WorkSource; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.location.internal.ClientIdentity; +import com.google.android.gms.tasks.CancellationToken; +import org.microg.gms.common.Hide; +import org.microg.gms.common.PublicApi; +import org.microg.gms.location.GranularityUtil; +import org.microg.gms.location.PriorityUtil; +import org.microg.safeparcel.AutoSafeParcelable; + +/** + * An encapsulation of various parameters for requesting the current location through FusedLocationProviderClient. + * + * @see FusedLocationProviderClient#getCurrentLocation(CurrentLocationRequest, CancellationToken) + */ +@PublicApi +public class CurrentLocationRequest extends AutoSafeParcelable { + @Field(1) + private long maxUpdateAgeMillis; + @Field(2) + private @Granularity int granularity; + @Field(3) + private @Priority int priority; + @Field(4) + private long durationMillis; + @Field(5) + private boolean bypass; + @Field(6) + private WorkSource workSource; + @Field(7) + private @ThrottleBehavior int throttleBehavior; + @Field(8) + private String moduleId; + @Field(9) + private ClientIdentity impersonation; + + private CurrentLocationRequest() { + maxUpdateAgeMillis = Long.MAX_VALUE; + granularity = Granularity.GRANULARITY_PERMISSION_LEVEL; + priority = Priority.PRIORITY_BALANCED_POWER_ACCURACY; + durationMillis = Long.MAX_VALUE; + workSource = new WorkSource(); + throttleBehavior = ThrottleBehavior.THROTTLE_BACKGROUND; + } + + /** + * The duration in milliseconds of the location request used to derive the current location if no historical location satisfies the current location + * request. If this duration expires with no location, the current location request will return a null location. The current location request may fail and + * return a null location after a shorter duration (ie, the given duration may be capped internally), but never a longer duration. + */ + public long getDurationMillis() { + return durationMillis; + } + + /** + * The {@link Granularity} of locations returned for this request. This controls whether fine or coarse locations may be returned. + */ + public @Granularity int getGranularity() { + return granularity; + } + + /** + * The maximum age of any location returned for this request. A value of 0 indicates that only freshly derived locations will be returned, and no + * historical locations will ever be returned. A value Long.MAX_VALUE represents an effectively unbounded maximum age. + *

+ * NOTE: This parameter applies only to historical locations. Freshly derived locations should almost always have timestamps close to the present time - + * however it is possible under unlikely conditions for location derivation to take longer than expected, in which case freshly derived locations may have + * slightly older timestamps. + */ + public long getMaxUpdateAgeMillis() { + return maxUpdateAgeMillis; + } + + /** + * The {@link Priority} of the location request used to derive the current location if no historical location satisfies the current location request. + */ + public @Priority int getPriority() { + return priority; + } + + /** + * A builder for {@link CurrentLocationRequest}. + */ + public static class Builder { + private long maxUpdateAgeMillis; + private @Granularity int granularity; + private @Priority int priority; + private long durationMillis; + private boolean bypass; + private @ThrottleBehavior int throttleBehavior; + @Nullable + private String moduleId; + @Nullable + private WorkSource workSource; + @Nullable + private ClientIdentity impersonation; + + /** + * Constructs a new builder with default values. + */ + public Builder() { + this.maxUpdateAgeMillis = 60000L; + this.granularity = Granularity.GRANULARITY_PERMISSION_LEVEL; + this.priority = Priority.PRIORITY_BALANCED_POWER_ACCURACY; + this.durationMillis = Long.MAX_VALUE; + this.bypass = false; + this.throttleBehavior = ThrottleBehavior.THROTTLE_BACKGROUND; + this.moduleId = null; + this.workSource = null; + this.impersonation = null; + } + + /** + * Constructs a new builder with values copied from the given {@link CurrentLocationRequest}. + */ + public Builder(CurrentLocationRequest request) { + this.maxUpdateAgeMillis = request.getMaxUpdateAgeMillis(); + this.granularity = request.getGranularity(); + this.priority = request.getPriority(); + this.durationMillis = request.getDurationMillis(); + this.bypass = request.isBypass(); + this.throttleBehavior = request.getThrottleBehavior(); + this.moduleId = request.getModuleId(); + this.workSource = new WorkSource(request.getWorkSource()); + this.impersonation = request.getImpersonation(); + } + + /** + * Builds a new {@link CurrentLocationRequest}. + */ + @NonNull + public CurrentLocationRequest build() { + CurrentLocationRequest request = new CurrentLocationRequest(); + request.maxUpdateAgeMillis = maxUpdateAgeMillis; + request.granularity = granularity; + request.priority = priority; + request.durationMillis = durationMillis; + request.bypass = bypass; + request.throttleBehavior = throttleBehavior; + request.moduleId = moduleId; + request.workSource = new WorkSource(workSource); + request.impersonation = impersonation; + return request; + } + + /** + * Sets the duration in milliseconds of the location request used to derive the current location if no historical location satisfies the current + * location request. If this duration expires with no location, the current location request will return a null location. The current location request + * may fail and return a null location after a shorter duration, but never a longer duration. + *

+ * NOTE: Internally, this duration may be capped with what the Fused Location Provider believes is a reasonable maximum duration until it is unlikely + * that any current location can be derived. This value is usually around roughly 30 seconds. + *

+ * The default value is {@link Long#MAX_VALUE}. + */ + public CurrentLocationRequest.Builder setDurationMillis(long durationMillis) { + if (durationMillis <= 0) throw new IllegalArgumentException("durationMillis must be greater than 0"); + this.durationMillis = durationMillis; + return this; + } + + /** + * Sets the {@link Granularity} of locations returned for this request. This controls whether fine or coarse locations may be returned. + *

+ * The default value is {@link Granularity#GRANULARITY_PERMISSION_LEVEL}. + */ + public CurrentLocationRequest.Builder setGranularity(@Granularity int granularity) { + GranularityUtil.checkValidGranularity(granularity); + this.granularity = granularity; + return this; + } + + /** + * Sets the maximum age of any location returned for this request. A value of 0 indicates that only freshly derived locations will be returned, and no + * historical locations will ever be returned. A value {@link Long#MAX_VALUE} represents an effectively unbounded maximum age. + *

+ * NOTE: This parameter applies only to historical locations. Freshly derived locations should almost always have timestamps close to the present time - + * however it is possible under unlikely conditions for location derivation to take longer than expected, in which case freshly derived locations may + * have slightly older timestamps. + *

+ * The default value is 1 minute. Do not rely on the default value always being 1 minute as this may change without notice. + */ + public CurrentLocationRequest.Builder setMaxUpdateAgeMillis(long maxUpdateAgeMillis) { + if (maxUpdateAgeMillis < 0) throw new IllegalArgumentException("maxUpdateAgeMillis must be greater than or equal to 0"); + this.maxUpdateAgeMillis = maxUpdateAgeMillis; + return this; + } + + /** + * Sets the {@link Priority} of the location request used to derive the current location if no historical location satisfies the current location + * request. + *

+ * The default value is {@link Priority#PRIORITY_BALANCED_POWER_ACCURACY}. + */ + public CurrentLocationRequest.Builder setPriority(@Priority int priority) { + PriorityUtil.checkValidPriority(priority); + this.priority = priority; + return this; + } + + @Hide + public CurrentLocationRequest.Builder setBypass(boolean bypass) { + this.bypass = bypass; + return this; + } + + @Hide + public CurrentLocationRequest.Builder setThrottleBehavior(int throttleBehavior) { + this.throttleBehavior = throttleBehavior; + return this; + } + + @Hide + public CurrentLocationRequest.Builder setModuleId(@Nullable String moduleId) { + this.moduleId = moduleId; + return this; + } + + @Hide + public CurrentLocationRequest.Builder setWorkSource(@Nullable WorkSource workSource) { + this.workSource = workSource; + return this; + } + + @Hide + public CurrentLocationRequest.Builder setImpersonation(@Nullable ClientIdentity impersonation) { + this.impersonation = impersonation; + return this; + } + } + + @Hide + public boolean isBypass() { + return bypass; + } + + @Hide + public @ThrottleBehavior int getThrottleBehavior() { + return throttleBehavior; + } + + @Hide + public String getModuleId() { + return moduleId; + } + + @Hide + public WorkSource getWorkSource() { + return workSource; + } + + @Hide + public ClientIdentity getImpersonation() { + return impersonation; + } + + public static final Creator CREATOR = new AutoCreator<>(CurrentLocationRequest.class); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/DetectedActivity.java b/play-services-location/src/main/java/com/google/android/gms/location/DetectedActivity.java index 4a75a4d272..c88b1ce6b0 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/DetectedActivity.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/DetectedActivity.java @@ -1,28 +1,21 @@ /* - * Copyright (C) 2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2017 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.location; +import org.microg.gms.common.Hide; import org.microg.gms.common.PublicApi; import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; /** - * The detected activity of the device with an an associated confidence. See ActivityRecognitionApi - * for details on how to obtain a DetectedActivity. + * The detected activity of the device with an associated confidence. See {@link ActivityRecognitionApi} + * for details on how to obtain a {@link DetectedActivity}. */ @PublicApi public class DetectedActivity extends AutoSafeParcelable { @@ -68,13 +61,13 @@ public class DetectedActivity extends AutoSafeParcelable { */ public static final int WALKING = 7; - @SafeParceled(1000) + @Field(1000) private int versionCode = 1; - @SafeParceled(1) + @Field(1) private int type; - @SafeParceled(2) + @Field(2) private int confidence; private DetectedActivity() { @@ -92,13 +85,6 @@ public DetectedActivity(int activityType, int confidence) { this.confidence = confidence; } - @PublicApi(exclude = true) - public DetectedActivity(int versionCode, int type, int confidence) { - this.versionCode = versionCode; - this.type = type; - this.confidence = confidence; - } - /** * Returns a value from 0 to 100 indicating the likelihood that the user is performing this * activity. @@ -127,17 +113,12 @@ public int getType() { return type; } - @PublicApi(exclude = true) - public int getVersionCode() { - return versionCode; - } - @Override public String toString() { return "DetectedActivity [type=" + typeToString(getType()) + ", confidence=" + getConfidence() + "]"; } - @PublicApi(exclude = true) + @Hide public static String typeToString(int type) { switch (type) { case 0: diff --git a/play-services-location/src/main/java/com/google/android/gms/location/DeviceOrientation.java b/play-services-location/src/main/java/com/google/android/gms/location/DeviceOrientation.java index 61373df057..4aa022ecbb 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/DeviceOrientation.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/DeviceOrientation.java @@ -5,10 +5,13 @@ package com.google.android.gms.location; +import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; import org.microg.safeparcel.AutoSafeParcelable; import java.util.Arrays; +@Hide public class DeviceOrientation extends AutoSafeParcelable { @Field(1) private float[] attitude = new float[4]; @@ -21,97 +24,108 @@ public class DeviceOrientation extends AutoSafeParcelable { @Field(5) private float headingErrorDegrees = Float.NaN; @Field(6) - private long elapsedRealtimeNanos = 0; + public long elapsedRealtimeNanos = 0; @Field(7) - private byte flags = 0; + private byte fieldsMask = 0; @Field(8) private float conservativeHeadingErrorVonMisesKappa = Float.NaN; public float[] getAttitude() { - if ((flags & 0x10) != 0) return attitude; + if (hasAttitude()) return attitude; return new float[4]; } public void setAttitude(float[] attitude) { if (attitude.length != 4) throw new IllegalArgumentException(); this.attitude = attitude; - flags = (byte) (flags | 0x10); + fieldsMask = (byte) (fieldsMask | 0x10); } public int getAttitudeConfidence() { - if ((flags & 0x1) != 0) return attitudeConfidence; + if (hasAttitudeConfidence()) return attitudeConfidence; return -1; } public void setAttitudeConfidence(int attitudeConfidence) { this.attitudeConfidence = attitudeConfidence; - flags = (byte) (flags | 0x1); + fieldsMask = (byte) (fieldsMask | 0x1); } public int getMagConfidence() { - if ((flags & 0x2) != 0) return magConfidence; + if (hasMagConfidence()) return magConfidence; return -1; } public void setMagConfidence(int magConfidence) { this.magConfidence = magConfidence; - flags = (byte) (flags | 0x2); + fieldsMask = (byte) (fieldsMask | 0x2); } public float getHeadingDegrees() { - if ((flags & 0x4) != 0) return headingDegrees; + if (hasHeadingDegrees()) return headingDegrees; return Float.NaN; } public void setHeadingDegrees(float headingDegrees) { this.headingDegrees = headingDegrees; - flags = (byte) (flags | 0x4); + fieldsMask = (byte) (fieldsMask | 0x4); } public float getHeadingErrorDegrees() { - if ((flags & 0x8) != 0) return headingErrorDegrees; + if (hasHeadingErrorDegrees()) return headingErrorDegrees; return Float.NaN; } public void setHeadingErrorDegrees(float headingErrorDegrees) { this.headingErrorDegrees = headingErrorDegrees; - flags = (byte) (flags | 0x8); + fieldsMask = (byte) (fieldsMask | 0x8); } public float getConservativeHeadingErrorVonMisesKappa() { - if ((flags & 0x20) != 0) return conservativeHeadingErrorVonMisesKappa; + if (hasConservativeHeadingErrorVonMisesKappa()) return conservativeHeadingErrorVonMisesKappa; return Float.NaN; } public void setConservativeHeadingErrorVonMisesKappa(float conservativeHeadingErrorVonMisesKappa) { this.conservativeHeadingErrorVonMisesKappa = conservativeHeadingErrorVonMisesKappa; - flags = (byte) (flags | 0x20); + fieldsMask = (byte) (fieldsMask | 0x20); + } + + public final boolean hasAttitude() { + return (fieldsMask & 0x10) != 0; + } + + public final boolean hasAttitudeConfidence() { + return (fieldsMask & 0x1) != 0; + } + + public final boolean hasConservativeHeadingErrorVonMisesKappa() { + return (fieldsMask & 0x20) != 0; + } + + public final boolean hasHeadingDegrees() { + return (fieldsMask & 0x4) != 0; + } + + public final boolean hasHeadingErrorDegrees() { + return (fieldsMask & 0x8) != 0; + } + + public final boolean hasMagConfidence() { + return (fieldsMask & 0x2) != 0; } @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("DeviceOrientation{"); - if ((flags & 0x10) != 0) - sb.append("attitude=").append(Arrays.toString(attitude)); - if ((flags & 0x1) != 0) - sb.append(", attitudeConfidence=").append(attitudeConfidence); - if ((flags & 0x2) != 0) - sb.append(", magConfidence=").append(magConfidence); - if ((flags & 0x4) != 0) - sb.append(", headingDegrees=").append(headingDegrees); - if ((flags & 0x8) != 0) - sb.append(", headingErrorDegrees=").append(headingErrorDegrees); - return "DeviceOrientation{" + - "attitude=" + Arrays.toString(attitude) + - ", attitudeConfidence=" + attitudeConfidence + - ", magConfidence=" + magConfidence + - ", headingDegrees=" + headingDegrees + - ", headingErrorDegrees=" + headingErrorDegrees + - ", elapsedRealtimeNanos=" + elapsedRealtimeNanos + - ", flags=" + flags + - ", conservativeHeadingErrorVonMisesKappa=" + conservativeHeadingErrorVonMisesKappa + - '}'; + ToStringHelper helper = ToStringHelper.name("DeviceOrientation"); + if (hasAttitude()) helper.field("attitude", Arrays.toString(attitude)); + if (hasAttitudeConfidence()) helper.field("attitudeConfidence", attitudeConfidence); + if (hasMagConfidence()) helper.field("magConfidence", magConfidence); + if (hasHeadingDegrees()) helper.field("headingDegrees", headingDegrees); + if (hasHeadingErrorDegrees()) helper.field("headingErrorDegrees", headingErrorDegrees); + if (hasConservativeHeadingErrorVonMisesKappa()) helper.field("conservativeHeadingErrorVonMisesKappa", conservativeHeadingErrorVonMisesKappa); + helper.field("elapsedRealtimeNanos", elapsedRealtimeNanos); + return helper.end(); } public static final Creator CREATOR = new AutoCreator(DeviceOrientation.class); diff --git a/play-services-location/src/main/java/com/google/android/gms/location/DeviceOrientationRequest.java b/play-services-location/src/main/java/com/google/android/gms/location/DeviceOrientationRequest.java index 8326c457ee..0a15b961ee 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/DeviceOrientationRequest.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/DeviceOrientationRequest.java @@ -7,26 +7,28 @@ import android.os.SystemClock; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; +@Hide public class DeviceOrientationRequest extends AutoSafeParcelable { @Field(1) - public boolean shouldUseMag; + public boolean shouldUseMag = true; @Field(2) - public long minimumSamplingPeriodMs; + public long minimumSamplingPeriodMs = 50; @Field(3) - public float smallesAngleChangeRadians; + public float smallestAngleChangeRadians = 0.0f; @Field(4) - public long expirationTime; + public long expirationTime = Long.MAX_VALUE; @Field(5) - public int numUpdates; + public int numUpdates = Integer.MAX_VALUE; @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Request[shouldUseMag=").append(shouldUseMag); sb.append(" minimumSamplingPeriod=").append(minimumSamplingPeriodMs).append("ms"); - sb.append(" smallesAngleChange=").append(smallesAngleChangeRadians).append("rad"); + sb.append(" smallesAngleChange=").append(smallestAngleChangeRadians).append("rad"); if (expirationTime != Long.MAX_VALUE) sb.append(" expireIn=").append(expirationTime - SystemClock.elapsedRealtime()).append("ms"); if (numUpdates != Integer.MAX_VALUE) diff --git a/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderApi.java b/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderApi.java index 985fcbd43f..6d1b9f723d 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderApi.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderApi.java @@ -11,13 +11,21 @@ import android.app.PendingIntent; import android.content.Intent; import android.location.Location; +import android.location.LocationManager; import android.os.Bundle; import android.os.Looper; +import android.provider.Settings; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.Status; +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; + /** * The main entry point for interacting with the fused location provider. *

@@ -35,6 +43,7 @@ public interface FusedLocationProviderApi { * {@link LocationAvailability#hasLocationAvailability(Intent)} and {@link LocationAvailability#extractLocationAvailability(Intent)}. */ @Deprecated + @NonNull String KEY_LOCATION_CHANGED = "com.google.android.location.LOCATION"; /** * Key used for the Bundle extra in {@link Location} object holding a boolean indicating whether the location was @@ -42,6 +51,7 @@ public interface FusedLocationProviderApi { * * @deprecated Prefer to use {@link LocationCompat#isMock()} from the compat libraries. */ + @NonNull String KEY_MOCK_LOCATION = "mockLocation"; /** @@ -51,6 +61,7 @@ public interface FusedLocationProviderApi { *

* When the returned {@link PendingResult} is complete, then you can assume that any pending batched locations have already been delivered. */ + @NonNull PendingResult flushLocations(GoogleApiClient client); /** @@ -64,6 +75,8 @@ public interface FusedLocationProviderApi { * * @param client An existing GoogleApiClient. If not connected null will be returned. */ + @Nullable + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) Location getLastLocation(GoogleApiClient client); /** @@ -78,6 +91,8 @@ public interface FusedLocationProviderApi { * * @param client An existing GoogleApiClient. If not connected null will be returned. */ + @Nullable + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) LocationAvailability getLocationAvailability(GoogleApiClient client); /** @@ -91,21 +106,163 @@ public interface FusedLocationProviderApi { * or is equal as defined by {@link PendingIntent#equals(Object)}. * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it was successful. */ + @NonNull PendingResult removeLocationUpdates(GoogleApiClient client, PendingIntent callbackIntent); + /** + * Removes all location updates for the given location listener. + * + * @param client An existing GoogleApiClient. It must be connected at the time of this call, which is normally achieved by calling + * {@link GoogleApiClient#connect()} and waiting for {@link GoogleApiClient.ConnectionCallbacks#onConnected} to be called. + * @param listener The listener to remove. + * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it was successful. + */ + @NonNull PendingResult removeLocationUpdates(GoogleApiClient client, LocationListener listener); + /** + * Removes all location updates for the given location result listener. + * + * @param client An existing GoogleApiClient. It must be connected at the time of this call, which is normally achieved by calling + * {@link GoogleApiClient#connect()} and waiting for {@link GoogleApiClient.ConnectionCallbacks#onConnected} to be called. + * @param callback The callback to remove. + * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it was successful. + */ + @NonNull PendingResult removeLocationUpdates(GoogleApiClient client, LocationCallback callback); + /** + * Requests location updates. + *

+ * This method is suited for the foreground use cases, more specifically for requesting locations while being connected to {@link GoogleApiClient}. For + * background use cases, the {@link PendingIntent} version of the method is recommended, see + * {@link #requestLocationUpdates(GoogleApiClient, LocationRequest, PendingIntent)}. + *

+ * Any previous LocationRequests registered on this LocationListener will be replaced. + *

+ * Callbacks for LocationListener will be made on the calling thread, which must already be a prepared looper thread, such as the main thread of the + * calling Activity. The variant of this method with a {@link Looper} is recommended for cases where the callback needs to happen on a specific thread. See + * {@link #requestLocationUpdates(GoogleApiClient, LocationRequest, LocationListener, Looper)}. + * + * @param client An existing GoogleApiClient. It must be connected at the time of this call, which is normally achieved by calling + * {@link GoogleApiClient#connect()} and waiting for {@link GoogleApiClient.ConnectionCallbacks#onConnected} to be called. + * @param request The location request for the updates. + * @param listener The listener for the location updates. + * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it was successful. + * @throws IllegalStateException If this method is executed in a thread that has not called Looper.prepare(). + */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) PendingResult requestLocationUpdates(GoogleApiClient client, LocationRequest request, LocationListener listener); + /** + * Requests location updates with a callback on the specified Looper thread. + *

+ * This method is suited for the foreground use cases,more specifically for requesting locations while being connected to {@link GoogleApiClient}. For + * background use cases, the {@link PendingIntent} version of the method is recommended, see + * {@link #requestLocationUpdates(GoogleApiClient, LocationRequest, PendingIntent)}. + *

+ * Any previous LocationRequests registered on this LocationListener will be replaced. + *

+ * Callbacks for {@link LocationCallback} will be made on the specified thread, which must already be a prepared looper thread. + * + * @param client An existing GoogleApiClient. It must be connected at the time of this call, which is normally achieved by calling + * {@link GoogleApiClient#connect()} and waiting for {@link GoogleApiClient.ConnectionCallbacks#onConnected} to be called. + * @param request The location request for the updates. + * @param callback The callback for the location updates. + * @param looper The Looper object whose message queue will be used to implement the callback mechanism, or null to make callbacks on the calling thread. + * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it was successful. + * @throws IllegalStateException If looper is null and this method is executed in a thread that has not called Looper.prepare(). + */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) PendingResult requestLocationUpdates(GoogleApiClient client, LocationRequest request, LocationCallback callback, Looper looper); + /** + * Requests location updates with a callback on the specified Looper thread. + *

+ * This method is suited for the foreground use cases,more specifically for requesting locations while being connected to {@link GoogleApiClient}. For + * background use cases, the {@link PendingIntent} version of the method is recommended, see + * {@link #requestLocationUpdates(GoogleApiClient, LocationRequest, PendingIntent)}. + *

+ * Any previous LocationRequests registered on this LocationListener will be replaced. + *

+ * Callbacks for LocationListener will be made on the specified thread, which must already be a prepared looper thread. For cases where the callback can + * happen on the calling thread, the variant of this method without a {@link Looper} can be used. + * + * @param client An existing GoogleApiClient. It must be connected at the time of this call, which is normally achieved by calling + * {@link GoogleApiClient#connect()} and waiting for {@link GoogleApiClient.ConnectionCallbacks#onConnected} to be called. + * @param request The location request for the updates. + * @param listener The listener for the location updates. + * @param looper The Looper object whose message queue will be used to implement the callback mechanism, or null to make callbacks on the calling thread. + * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it was successful. + * @throws IllegalStateException If looper is null and this method is executed in a thread that has not called Looper.prepare(). + */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) PendingResult requestLocationUpdates(GoogleApiClient client, LocationRequest request, LocationListener listener, Looper looper); + /** + * Requests location updates with a callback on the specified PendingIntent. + *

+ * This method is suited for the background use cases, more specifically for receiving location updates, even when the app has been killed by the system. + * In order to do so, use a {@link PendingIntent} for a started service. For foreground use cases, the {@link LocationListener} version of the method is + * recommended, see {@link #requestLocationUpdates(GoogleApiClient, LocationRequest, LocationListener)}. + *

+ * Any previously registered requests that have the same PendingIntent (as defined by {@link PendingIntent#equals(Object)}) will be replaced by this + * request. + *

+ * Both {@link LocationResult} and {@link LocationAvailability} are sent to the given PendingIntent. You can extract data from an Intent using + * {@link LocationResult#hasResult(Intent)}, {@link LocationResult#extractResult(Intent)}, {@link LocationAvailability#hasLocationAvailability(Intent)}, + * and {@link LocationAvailability#extractLocationAvailability(Intent)}. + * + * @param client An existing GoogleApiClient. It must be connected at the time of this call, which is normally achieved by calling + * {@link GoogleApiClient#connect()} and waiting for {@link GoogleApiClient.ConnectionCallbacks#onConnected} to be called. + * @param request The location request for the updates. + * @param callbackIntent A pending intent to be sent for each location update. + * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it was successful. + */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) PendingResult requestLocationUpdates(GoogleApiClient client, LocationRequest request, PendingIntent callbackIntent); + /** + * Sets the mock location to be used for the location provider. This location will be used in place of any actual locations from the underlying providers + * (network or gps). + *

+ * {@link #setMockMode(GoogleApiClient, boolean)} must be called and set to true prior to calling this method. + *

+ * Care should be taken in specifying the timestamps as many applications require them to be monotonically increasing. + * + * @param client An existing GoogleApiClient. It must be connected at the time of this call, which is normally achieved by calling + * {@link GoogleApiClient#connect()} and waiting for {@link GoogleApiClient.ConnectionCallbacks#onConnected} to be called. + * @param mockLocation The mock location. Must have a minimum number of fields set to be considered a valid location, as per documentation in the + * {@link Location} class. + * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it was successful. + * @throws SecurityException if the ACCESS_MOCK_LOCATION permission is not present or the {@link Settings.Secure#ALLOW_MOCK_LOCATION} system setting is + * not enabled. + */ + @NonNull PendingResult setMockLocation(GoogleApiClient client, Location mockLocation); + /** + * Sets whether or not the location provider is in mock mode. + *

+ * The underlying providers (network and gps) will be stopped (except by direct {@link LocationManager} access), and only locations specified in + * {@link #setMockLocation(GoogleApiClient, Location)} will be reported. This will affect all location clients connected using the + * {@link FusedLocationProviderApi}, including geofencer clients (i.e. geofences can be triggered based on mock locations). + *

+ * The client must remain connected in order for mock mode to remain active. If the client dies the system will return to its normal state. + *

+ * Calls are not nested, and mock mode will be set directly regardless of previous calls. + * + * @param client An existing GoogleApiClient. It must be connected at the time of this call, which is normally achieved by calling + * {@link GoogleApiClient#connect()} and waiting for {@link GoogleApiClient.ConnectionCallbacks#onConnected} to be called. + * @param isMockMode If true the location provider will be set to mock mode. If false it will be returned to its normal state. + * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it was successful. + * @throws SecurityException if the ACCESS_MOCK_LOCATION permission is not present or the {@link Settings.Secure#ALLOW_MOCK_LOCATION} system setting is + * not enabled. + */ + @NonNull PendingResult setMockMode(GoogleApiClient client, boolean isMockMode); } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderClient.java b/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderClient.java index bfecc386bd..033eaa669b 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderClient.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderClient.java @@ -15,14 +15,21 @@ import android.location.Location; import android.os.Looper; +import android.provider.Settings; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresPermission; import com.google.android.gms.common.api.Api; import com.google.android.gms.common.api.GoogleApi; +import com.google.android.gms.tasks.CancellationToken; import com.google.android.gms.tasks.Task; import org.microg.gms.common.PublicApi; import java.util.concurrent.Executor; +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; + /** * The main entry point for interacting with the Fused Location Provider (FLP). In order to obtain an instance of this * class, see {@link LocationServices}. @@ -66,6 +73,7 @@ protected FusedLocationProviderClient(Context context) { * from the compat libraries instead. */ @Deprecated + @NonNull public static String KEY_MOCK_LOCATION = "mockLocation"; /** @@ -76,6 +84,7 @@ protected FusedLocationProviderClient(Context context) { * {@link LocationCompat#getVerticalAccuracyMeters()} from the compat libraries instead. */ @Deprecated + @NonNull public static String KEY_VERTICAL_ACCURACY = "verticalAccuracy"; /** @@ -86,28 +95,77 @@ protected FusedLocationProviderClient(Context context) { * When the returned {@link Task} is complete, then you can assume that any pending batched locations have already been * delivered. */ + @NonNull public abstract Task flushLocations(); /** - * Returns the most recent historical location currently available. Will return null if no historical location is available. The - * historical location may be of an arbitrary age, so clients should check how old the location is to see if it suits their - * purposes. + * Returns a single location fix representing the best estimate of the current location of the device. This may return a cached location if a recent enough + * location fix exists, or may compute a fresh location. If unable to retrieve a current location fix, null will be returned. + *

+ * Clients may supply an optional {@link CancellationToken} which may be used to cancel the request. + * + * @param priority {@link Priority} used to obtain location + * @param cancellationToken optional {@link CancellationToken} to cancel the request + */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) + public abstract Task getCurrentLocation(int priority, CancellationToken cancellationToken); + + /** + * Returns a single location fix representing the best estimate of the current location of the device. This may return a historical location if a recent + * enough location fix exists, or may compute a fresh location. If unable to retrieve a current location fix, null will be returned. + *

+ * Clients may supply an optional {@link CancellationToken} which may be used to cancel the request. + * + * @param request {@link CurrentLocationRequest} with parameters detailing how to obtain the current location + * @param cancellationToken optional {@link CancellationToken} to cancel the request */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) + public abstract Task getCurrentLocation(CurrentLocationRequest request, CancellationToken cancellationToken); + + /** + * Returns the most recent historical location currently available according to the given request. Will return null if no matching historical location is + * available. + */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) + public abstract Task getLastLocation(LastLocationRequest request); + + /** + * Returns the most recent historical location currently available. Will return null if no historical location is available. The historical location may + * be of an arbitrary age, so clients should check how old the location is to see if it suits their purposes. + */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) public abstract Task getLastLocation(); + /** + * Returns the estimated availability of location data. If {@link LocationAvailability#isLocationAvailable()} returns true then it is likely (but not + * guaranteed) that Fused Location Provider APIs will be able to derive and return fresh location updates. If + * {@link LocationAvailability#isLocationAvailable()} returns false, then it is likely (but not guaranteed) that Fused Location Provider APIs will be + * unable to derive and return fresh location updates, though there may be historical locations available. + */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) + public abstract Task getLocationAvailability(); + /** * Removes all location updates for the given listener. */ + @NonNull public abstract Task removeLocationUpdates(LocationListener listener); /** * Removes all location updates for the given callback. */ + @NonNull public abstract Task removeLocationUpdates(LocationCallback callback); /** * Removes all location updates for the given pending intent. */ + @NonNull public abstract Task removeLocationUpdates(PendingIntent pendingIntent); /** @@ -124,6 +182,8 @@ protected FusedLocationProviderClient(Context context) { * * @throws IllegalStateException if {@code looper} is null and the calling thread has not called {@link Looper#prepare()} */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) public abstract Task requestLocationUpdates(LocationRequest request, LocationListener listener, Looper looper); /** @@ -137,6 +197,8 @@ protected FusedLocationProviderClient(Context context) { * Depending on the arguments passed in through the {@link LocationRequest}, locations from the past may be delivered when * the callback is first registered. Clients should ensure they are checking location timestamps appropriately if necessary. */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) public abstract Task requestLocationUpdates(LocationRequest request, Executor executor, LocationCallback callback); /** @@ -144,6 +206,8 @@ protected FusedLocationProviderClient(Context context) { * * @see #requestLocationUpdates(LocationRequest, LocationListener, Looper) */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) public abstract Task requestLocationUpdates(LocationRequest request, Executor executor, LocationListener listener); /** @@ -163,6 +227,8 @@ protected FusedLocationProviderClient(Context context) { * * @throws IllegalStateException if looper is null and the calling thread has not called {@link Looper#prepare()} */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) public abstract Task requestLocationUpdates(LocationRequest request, LocationCallback callback, Looper looper); /** @@ -188,5 +254,40 @@ protected FusedLocationProviderClient(Context context) { * Depending on the arguments passed in through the {@link LocationRequest}, locations from the past may be delivered when * the callback is first registered. Clients should ensure they are checking location timestamps appropriately if necessary. */ + @NonNull + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) public abstract Task requestLocationUpdates(LocationRequest request, PendingIntent pendingIntent); + + /** + * Sets the mock location of the Fused Location Provider. + *

+ * Delivers the given location to the FLP as if it was coming from an underlying location source. Normal FLP logic around receiving and delivering location + * will generally apply. For this reason the timestamps of the location should be set appropriately, as the FLP may expect monotonically increasing + * timestamps. When this location is reported to FLP clients it will be marked as a mock location (see {@link Location#isMock()} or + * {@link LocationCompat#isMock()} from the compat libraries). + *

+ * This API can only be successfully used while the FLP is in mock mode. Clients must fulfill the same security requirements as for + * {@link #setMockMode(boolean)} as well. + * + * @param location valid location to set as the next FLP location + * @throws SecurityException if security requirements are not met + */ + public abstract Task setMockLocation(Location location); + + /** + * Sets whether or not the Fused Location Provider is in mock mode. + *

+ * Entering mock mode clears the FLP's cached locations, and ensures that the FLP will only report locations set through {@link #setMockLocation(Location)}. + * Exiting mock mode will clear any mock locations set from the FLP's cache as well. Mock mode affects all location clients using the FLP, including + * location clients in other processes and derivative APIs such as geofencing and so forth. Because this affects all FLP usage, clients should always + * ensure they properly set the mock mode to false when finished. + *

+ * Successfully using this API on devices running Android M+ requires the client to request the {@code android.permission.ACCESS_MOCK_LOCATION} permission + * and to be selected as the mock location app within the device developer settings. Using this API on pre-M devices requires the + * {@link Settings.Secure#ALLOW_MOCK_LOCATION} setting to be enabled. + * + * @param mockMode the mock mode state to set for the Fused Location Provider APIs + * @throws SecurityException if security requirements are not met + */ + public abstract Task setMockMode(boolean mockMode); } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/Geofence.java b/play-services-location/src/main/java/com/google/android/gms/location/Geofence.java index 194aed757a..2e66b41833 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/Geofence.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/Geofence.java @@ -1,25 +1,25 @@ /* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2017 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.location; import android.os.SystemClock; +import androidx.annotation.FloatRange; +import androidx.annotation.IntDef; +import androidx.annotation.IntRange; import com.google.android.gms.location.internal.ParcelableGeofence; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + /** * Represents a geographical region, also known as a geofence. Geofences can be monitored by * geofencer service. And when the user crosses the boundary of a geofence, an alert will be @@ -43,6 +43,38 @@ public interface Geofence { */ long NEVER_EXPIRE = -1L; + /** + * Returns the expiration elapsed realtime of geofence in milliseconds, or {@link #NEVER_EXPIRE} if there's no expiration. When positive, this geofence will + * be removed automatically after that time. + */ + long getExpirationTime(); + + /** + * Returns latitude in degrees, between -90 and +90 inclusive. + */ + double getLatitude(); + + /** + * Returns the delay between {@link #GEOFENCE_TRANSITION_ENTER} and {@link #GEOFENCE_TRANSITION_DWELL} in milliseconds. + */ + int getLoiteringDelay(); + + /** + * Returns longitude in degrees, between -180 and +180 inclusive. + */ + double getLongitude(); + + /** + * Returns the best-effort description of how soon should the callback be called when the transition associated with the geofence is triggered, + * in milliseconds. + */ + int getNotificationResponsiveness(); + + /** + * Returns radius in meters. + */ + float getRadius(); + /** * Returns the request ID of this geofence. The request ID is a string to identify this geofence * inside your application. When two geofences with the same requestId are monitored, the new @@ -51,6 +83,11 @@ public interface Geofence { */ String getRequestId(); + /** + * Returns the transition types of interest as a bitwise-OR of {@code GEOFENCE_TRANSITION_} flags. + */ + @TransitionTypes int getTransitionTypes(); + /** * A builder that builds {@link Geofence}. */ @@ -63,7 +100,7 @@ class Builder { private int loiteringDelay = -1; private int notificationResponsiveness; private String requestId; - private int transitionTypes; + private @TransitionTypes int transitionTypes; /** * Creates a geofence object. @@ -95,7 +132,7 @@ public Geofence build() throws IllegalArgumentException { * @param longitude longitude in degrees, between -180 and +180 inclusive * @param radius radius in meters */ - public Builder setCircularRegion(double latitude, double longitude, float radius) { + public Builder setCircularRegion(@FloatRange(from = -90.0d, to = 90.0d) double latitude, @FloatRange(from = -180.0d, to = 180.0d) double longitude, @FloatRange(from = 0.0d, fromInclusive = false) float radius) { this.regionType = 1; this.latitude = latitude; this.longitude = longitude; @@ -139,7 +176,7 @@ public Builder setLoiteringDelay(int loiteringDelayMs) { * to 300000 milliseconds the callback will be called 5 * minutes within entering or exiting the geofence. */ - public Builder setNotificationResponsiveness(int notificationResponsivenessMs) { + public Builder setNotificationResponsiveness(@IntRange(from = 0) int notificationResponsivenessMs) { this.notificationResponsiveness = notificationResponsivenessMs; return this; } @@ -164,9 +201,29 @@ public Builder setRequestId(String requestId) { * @param transitionTypes geofence transition types of interest, as a bitwise-OR of * GEOFENCE_TRANSITION_ flags. */ - public Builder setTransitionTypes(int transitionTypes) { + public Builder setTransitionTypes(@TransitionTypes int transitionTypes) { this.transitionTypes = transitionTypes; return this; } } + + /** + * Geofence transition event. + */ + @Target({ElementType.TYPE_USE}) + @Retention(RetentionPolicy.SOURCE) + @IntDef({GEOFENCE_TRANSITION_ENTER, GEOFENCE_TRANSITION_EXIT, GEOFENCE_TRANSITION_DWELL}) + @interface GeofenceTransition { + + } + + /** + * Geofence transition types of interest, as either 0 or a bitwise-OR of {@code GEOFENCE_TRANSITION_} flags. + */ + @Target({ElementType.TYPE_USE}) + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {GEOFENCE_TRANSITION_ENTER, GEOFENCE_TRANSITION_EXIT, GEOFENCE_TRANSITION_DWELL}, flag = true) + @interface TransitionTypes { + + } } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/GeofenceStatusCodes.java b/play-services-location/src/main/java/com/google/android/gms/location/GeofenceStatusCodes.java index 8733e4cb21..4fcd05c61f 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/GeofenceStatusCodes.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/GeofenceStatusCodes.java @@ -1,42 +1,47 @@ /* - * Copyright (C) 2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2017 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.location; +import android.app.PendingIntent; import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +/** + * Geofence specific status codes, for use in {@link Status#getStatusCode()} + */ public class GeofenceStatusCodes extends CommonStatusCodes { /** - * Geofence service is not available now. Typically this is because the user turned off - * location access in settings > location access. + * Geofence service is not available now. Typically this is because the user turned off location access in settings > location access. */ public static final int GEOFENCE_NOT_AVAILABLE = 1000; /** - * Your app has registered more than 100 geofences. Remove unused ones before adding new - * geofences. + * Your app has registered more than 100 geofences. Remove unused ones before adding new geofences. */ public static final int GEOFENCE_TOO_MANY_GEOFENCES = 1001; /** - * You have provided more than 5 different PendingIntents to the addGeofences(GoogleApiClient, - * GeofencingRequest, PendingIntent) call. + * You have provided more than 5 different PendingIntents to the {@link GeofencingApi#addGeofences(GoogleApiClient, GeofencingRequest, PendingIntent)} call. */ public static final int GEOFENCE_TOO_MANY_PENDING_INTENTS = 1002; + /** + * The client doesn't have sufficient location permission to perform geofencing operations. + */ + public static final int GEOFENCE_INSUFFICIENT_LOCATION_PERMISSION = 1004; + + /** + * Your app has been adding Geofences too frequently. + */ + public static final int GEOFENCE_REQUEST_TOO_FREQUENT = 1005; + /** * Returns an untranslated debug (not user-friendly!) string based on the current status code. */ @@ -48,6 +53,10 @@ public static String getStatusCodeString(int statusCode) { return "GEOFENCE_TOO_MANY_GEOFENCES"; case GEOFENCE_TOO_MANY_PENDING_INTENTS: return "GEOFENCE_TOO_MANY_PENDING_INTENTS"; + case GEOFENCE_INSUFFICIENT_LOCATION_PERMISSION: + return "GEOFENCE_INSUFFICIENT_LOCATION_PERMISSION"; + case GEOFENCE_REQUEST_TOO_FREQUENT: + return "GEOFENCE_REQUEST_TOO_FREQUENT"; default: return CommonStatusCodes.getStatusCodeString(statusCode); } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/GeofencingApi.java b/play-services-location/src/main/java/com/google/android/gms/location/GeofencingApi.java index 9efc66ce5b..d0d9e66692 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/GeofencingApi.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/GeofencingApi.java @@ -17,13 +17,16 @@ package com.google.android.gms.location; import android.app.PendingIntent; - +import androidx.annotation.NonNull; +import androidx.annotation.RequiresPermission; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.Status; import java.util.List; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; + /** * The main entry point for interacting with the geofencing APIs. *

@@ -38,12 +41,18 @@ */ @Deprecated public interface GeofencingApi { + @NonNull + @RequiresPermission(ACCESS_FINE_LOCATION) PendingResult addGeofences(GoogleApiClient client, GeofencingRequest geofencingRequest, PendingIntent pendingIntent); @Deprecated + @NonNull + @RequiresPermission(ACCESS_FINE_LOCATION) PendingResult addGeofences(GoogleApiClient client, List geofences, PendingIntent pendingIntent); + @NonNull PendingResult removeGeofences(GoogleApiClient client, List geofenceRequestIds); + @NonNull PendingResult removeGeofences(GoogleApiClient client, PendingIntent pendingIntent); } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/GeofencingClient.java b/play-services-location/src/main/java/com/google/android/gms/location/GeofencingClient.java index 52139e0ffe..63e8873d70 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/GeofencingClient.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/GeofencingClient.java @@ -10,13 +10,32 @@ import android.app.Activity; +import android.app.PendingIntent; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresPermission; import com.google.android.gms.common.api.Api; import com.google.android.gms.common.api.HasApiKey; +import com.google.android.gms.tasks.Task; + +import java.util.List; + +import static android.Manifest.permission.ACCESS_FINE_LOCATION; /** * The main entry point for interacting with the geofencing APIs. + *

* Get an instance of this client via {@link LocationServices#getGeofencingClient(Activity)}. + *

* All methods are thread safe. */ public interface GeofencingClient extends HasApiKey { + @NonNull + @RequiresPermission(ACCESS_FINE_LOCATION) + Task addGeofences(GeofencingRequest geofencingRequest, PendingIntent pendingIntent); + + @NonNull + Task removeGeofences(List geofenceRequestIds); + + @NonNull + Task removeGeofences(PendingIntent pendingIntent); } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/GeofencingEvent.java b/play-services-location/src/main/java/com/google/android/gms/location/GeofencingEvent.java index 82b3a107d5..2be39c826b 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/GeofencingEvent.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/GeofencingEvent.java @@ -1,17 +1,9 @@ /* - * Copyright (C) 2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2017 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.location; @@ -21,6 +13,7 @@ import com.google.android.gms.location.internal.ParcelableGeofence; +import org.microg.gms.common.Hide; import org.microg.gms.common.PublicApi; import org.microg.safeparcel.SafeParcelUtil; @@ -35,17 +28,17 @@ */ @PublicApi public class GeofencingEvent { - @PublicApi(exclude = true) + @Hide public static final String EXTRA_ERROR_CODE = "gms_error_code"; - @PublicApi(exclude = true) + @Hide public static final String EXTRA_TRIGGERING_LOCATION = "com.google.android.location.intent.extra.triggering_location"; - @PublicApi(exclude = true) + @Hide public static final String EXTRA_TRANSITION = "com.google.android.location.intent.extra.transition"; - @PublicApi(exclude = true) + @Hide public static final String EXTRA_GEOFENCE_LIST = "com.google.android.location.intent.extra.geofence_list"; private int errorCode; - private int geofenceTransition; + private @Geofence.GeofenceTransition int geofenceTransition; private List triggeringGeofences; private Location triggeringLocation; @@ -93,7 +86,7 @@ public int getErrorCode() { * transition alert; Otherwise returns the GEOFENCE_TRANSITION_ flags value defined in * {@link Geofence}. */ - public int getGeofenceTransition() { + public @Geofence.GeofenceTransition int getGeofenceTransition() { return geofenceTransition; } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/GeofencingRequest.java b/play-services-location/src/main/java/com/google/android/gms/location/GeofencingRequest.java index 2601d629a9..1491708a5a 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/GeofencingRequest.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/GeofencingRequest.java @@ -1,27 +1,163 @@ /* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2017 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.location; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.location.internal.ParcelableGeofence; import org.microg.safeparcel.AutoSafeParcelable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.List; + /** - * TODO: usage + * Specifies the list of geofences to be monitored and how the geofence notifications should be reported. + *

+ * Refer to {@link GeofencingClient#addGeofences(GeofencingRequest, android.app.PendingIntent)} on how to monitor geofences. */ public class GeofencingRequest extends AutoSafeParcelable { + /** + * A flag indicating that geofencing service should trigger {@link Geofence#GEOFENCE_TRANSITION_ENTER} notification at the moment when the geofence is + * added and if the device is already inside that geofence. + */ + public static final int INITIAL_TRIGGER_ENTER = 1; + /** + * A flag indicating that geofencing service should trigger {@link Geofence#GEOFENCE_TRANSITION_EXIT} notification at the moment when the geofence is + * added and if the device is already outside that geofence. + */ + public static final int INITIAL_TRIGGER_EXIT = 2; + /** + * A flag indicating that geofencing service should trigger {@link Geofence#GEOFENCE_TRANSITION_DWELL} notification at the moment when the geofence is + * added and if the device is already inside that geofence for some time. + */ + public static final int INITIAL_TRIGGER_DWELL = 4; + + @Field(value = 1, subClass = ParcelableGeofence.class) + private List geofences; + @Field(2) + private @InitialTrigger int initialTrigger; + @Field(3) + private String tag = ""; + @Field(4) + @Nullable + private String contextAttributionTag; + + /** + * Gets the list of geofences to be monitored. + * + * @return the list of geofences to be monitored + */ + public List getGeofences() { + return geofences; + } + + /** + * Gets the triggering behavior at the moment when the geofences are added. + * + * @return the triggering behavior at the moment when the geofences are added. It's a bit-wise of {@link #INITIAL_TRIGGER_ENTER}, + * {@link #INITIAL_TRIGGER_EXIT}, and {@link #INITIAL_TRIGGER_DWELL}. + */ + public @InitialTrigger int getInitialTrigger() { + return initialTrigger; + } + + + /** + * The triggering behavior at the moment when the geofences are added. It's either 0, or a bit-wise OR of {@link GeofencingRequest#INITIAL_TRIGGER_ENTER}, + * {@link GeofencingRequest#INITIAL_TRIGGER_EXIT}, and {@link GeofencingRequest#INITIAL_TRIGGER_DWELL}. + */ + @Target({ElementType.TYPE_USE}) + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {INITIAL_TRIGGER_ENTER, INITIAL_TRIGGER_EXIT, INITIAL_TRIGGER_DWELL}, flag = true) + @interface InitialTrigger { + + } + + /** + * A builder that builds {@link GeofencingRequest}. + */ + public static class Builder { + private List geofences = new ArrayList<>(); + private @InitialTrigger int initialTrigger = INITIAL_TRIGGER_ENTER | INITIAL_TRIGGER_DWELL; + + /** + * Adds a geofence to be monitored by geofencing service. + * + * @param geofence the geofence to be monitored. The geofence must be built with {@link Geofence.Builder}. + * @return the builder object itself for method chaining + * @throws IllegalArgumentException if the geofence is not built with {@link Geofence.Builder}. + * @throws NullPointerException if the given geofence is null + */ + @NonNull + public GeofencingRequest.Builder addGeofence(Geofence geofence) { + if (geofence == null) throw new NullPointerException("geofence can't be null."); + if (!(geofence instanceof ParcelableGeofence)) throw new IllegalArgumentException("Geofence must be created using Geofence.Builder."); + geofences.add(geofence); + return this; + } + + /** + * Adds all the geofences in the given list to be monitored by geofencing service. + * + * @param geofences the geofences to be monitored. The geofences in the list must be built with {@link Geofence.Builder}. + * @return the builder object itself for method chaining + * @throws IllegalArgumentException if the geofence is not built with {@link Geofence.Builder}. + */ + @NonNull + public GeofencingRequest.Builder addGeofences(List geofences) { + if (geofences != null) { + for (Geofence geofence : geofences) { + if (geofence != null) addGeofence(geofence); + } + } + return this; + } + + /** + * Sets the geofence notification behavior at the moment when the geofences are added. The default behavior is + * {@link GeofencingRequest#INITIAL_TRIGGER_ENTER} and {@link GeofencingRequest#INITIAL_TRIGGER_DWELL}. + * + * @param initialTrigger the notification behavior. It's a bit-wise of {@link GeofencingRequest#INITIAL_TRIGGER_ENTER} and/or + * {@link GeofencingRequest#INITIAL_TRIGGER_EXIT} and/or {@link GeofencingRequest#INITIAL_TRIGGER_DWELL}. When + * {@code initialTrigger} is set to 0 ({@code setInitialTrigger(0)}), initial trigger would be disabled. + * @return the builder object itself for method chaining + */ + public GeofencingRequest.Builder setInitialTrigger(@InitialTrigger int initialTrigger) { + this.initialTrigger = initialTrigger; + return this; + } + + /** + * Builds the {@link GeofencingRequest} object. + * + * @return a {@link GeofencingRequest} object + * @throws IllegalArgumentException if no geofence has been added to this list + */ + public GeofencingRequest build() { + if (geofences.isEmpty()) throw new IllegalArgumentException("No geofence has been added to this request."); + GeofencingRequest request = new GeofencingRequest(); + request.geofences = geofences; + request.initialTrigger = initialTrigger; + return request; + } + } + + @NonNull + @Override + public String toString() { + return "GeofencingRequest[geofences=" + this.geofences + ", initialTrigger=" + this.initialTrigger + ", tag=" + this.tag + ", attributionTag=" + this.contextAttributionTag + "]"; + } - public static final Creator CREATOR = new AutoCreator(GeofencingRequest.class); + public static final Creator CREATOR = new AutoCreator<>(GeofencingRequest.class); } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/GestureRequest.java b/play-services-location/src/main/java/com/google/android/gms/location/GestureRequest.java index 5222b259c0..dd8cd0840f 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/GestureRequest.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/GestureRequest.java @@ -16,8 +16,10 @@ package com.google.android.gms.location; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; +@Hide public class GestureRequest extends AutoSafeParcelable { public static final Creator CREATOR = new AutoCreator(GestureRequest.class); } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LastLocationRequest.java b/play-services-location/src/main/java/com/google/android/gms/location/LastLocationRequest.java new file mode 100644 index 0000000000..8533d1b028 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LastLocationRequest.java @@ -0,0 +1,163 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.location.internal.ClientIdentity; +import org.microg.gms.common.Hide; +import org.microg.gms.location.GranularityUtil; +import org.microg.safeparcel.AutoSafeParcelable; + +import java.util.Objects; + +/** + * An encapsulation of various parameters for requesting a (cached) last location through {@link FusedLocationProviderClient}. + * + * @see FusedLocationProviderClient#getLastLocation(LastLocationRequest) + */ +public class LastLocationRequest extends AutoSafeParcelable { + @Field(1) + private long maxUpdateAgeMillis; + @Field(2) + private @Granularity int granularity; + @Field(3) + private boolean bypass; + @Field(4) + @Nullable + private String moduleId; + @Field(5) + @Nullable + private ClientIdentity impersonation; + + private LastLocationRequest() { + maxUpdateAgeMillis = Long.MAX_VALUE; + granularity = Granularity.GRANULARITY_PERMISSION_LEVEL; + } + + + /** + * The {@link Granularity} of locations returned for this request. This controls whether fine or coarse locations may be returned. + */ + public @Granularity int getGranularity() { + return granularity; + } + + /** + * The maximum age of any location returned for this request. A value of {@link Long#MAX_VALUE} represents an effectively unbounded maximum age. + */ + public long getMaxUpdateAgeMillis() { + return maxUpdateAgeMillis; + } + + /** + * A builder for {@link LastLocationRequest}. + */ + public static class Builder { + private long maxUpdateAgeMillis; + private @Granularity int granularity; + private boolean bypass; + @Nullable + private String moduleId; + @Nullable + private ClientIdentity impersonation; + + /** + * Constructs a new builder with default values. + */ + public Builder() { + maxUpdateAgeMillis = Long.MAX_VALUE; + granularity = Granularity.GRANULARITY_PERMISSION_LEVEL; + } + + /** + * Constructs a new builder with values copied from the given {@link LastLocationRequest}. + */ + public Builder(@NonNull LastLocationRequest request) { + this.maxUpdateAgeMillis = request.getMaxUpdateAgeMillis(); + this.granularity = request.getGranularity(); + this.bypass = request.isBypass(); + this.moduleId = request.getModuleId(); + this.impersonation = request.getImpersonation(); + } + + /** + * Sets the {@link Granularity} of locations returned for this request. This controls whether fine or coarse locations may be returned. + *

+ * The default value is {@link Granularity#GRANULARITY_PERMISSION_LEVEL}. + */ + public LastLocationRequest.Builder setGranularity(@Granularity int granularity) { + GranularityUtil.checkValidGranularity(granularity); + this.granularity = granularity; + return this; + } + + /** + * Sets the maximum age of any location returned for this request. A value of {@link Long#MAX_VALUE} represents an effectively unbounded maximum age. + *

+ * The default value is {@link Long#MAX_VALUE}. + */ + public LastLocationRequest.Builder setMaxUpdateAgeMillis(long maxUpdateAgeMillis) { + if (maxUpdateAgeMillis <= 0) throw new IllegalArgumentException("maxUpdateAgeMillis must be greater than 0"); + this.maxUpdateAgeMillis = maxUpdateAgeMillis; + return this; + } + + /** + * Builds a new {@link LastLocationRequest}. + */ + public LastLocationRequest build() { + LastLocationRequest request = new LastLocationRequest(); + request.maxUpdateAgeMillis = maxUpdateAgeMillis; + request.granularity = granularity; + request.bypass = bypass; + request.moduleId = moduleId; + request.impersonation = impersonation; + return request; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LastLocationRequest)) return false; + + LastLocationRequest request = (LastLocationRequest) o; + + if (maxUpdateAgeMillis != request.maxUpdateAgeMillis) return false; + if (granularity != request.granularity) return false; + if (bypass != request.bypass) return false; + if (!Objects.equals(moduleId, request.moduleId)) return false; + return Objects.equals(impersonation, request.impersonation); + } + + @Override + public int hashCode() { + return Objects.hashCode(new Object[]{this.maxUpdateAgeMillis, this.granularity, this.bypass}); + } + + @Hide + public boolean isBypass() { + return bypass; + } + + @Hide + @Nullable + public String getModuleId() { + return moduleId; + } + + @Hide + @Nullable + public ClientIdentity getImpersonation() { + return impersonation; + } + + public static final Creator CREATOR = new AutoCreator<>(LastLocationRequest.class); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationAvailability.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationAvailability.java index c24c5bbc15..7ad96f3cc1 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/LocationAvailability.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationAvailability.java @@ -1,23 +1,16 @@ /* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2015 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.location; import android.content.Intent; +import org.microg.gms.common.Hide; import org.microg.gms.common.PublicApi; import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; @@ -25,56 +18,69 @@ import java.util.Arrays; /** - * Status on the availability of location data. - *

- * Delivered from LocationCallback registered via FusedLocationProviderApi#requestLocationUpdates(GoogleApiClient, LocationRequest, LocationCallback, Looper) - * or from a PendingIntent registered via FusedLocationProviderApi#requestLocationUpdates(GoogleApiClient, LocationRequest, PendingIntent). - * It is also available on demand via FusedLocationProviderApi#getLocationAvailability(GoogleApiClient). + * Information on the availability of location data. */ @PublicApi public class LocationAvailability extends AutoSafeParcelable { private static final String EXTRA_KEY = "com.google.android.gms.location.EXTRA_LOCATION_AVAILABILITY"; - @PublicApi(exclude = true) + @Hide + public static LocationAvailability AVAILABLE = new LocationAvailability(0, 1, 1, 0, null); + @Hide + public static LocationAvailability UNAVAILABLE = new LocationAvailability(1000, 1, 1, 0, null); + + @Hide public static final int STATUS_SUCCESSFUL = 0; - @PublicApi(exclude = true) + @Hide public static final int STATUS_UNKNOWN = 1; - @PublicApi(exclude = true) + @Hide public static final int STATUS_TIMED_OUT_ON_SCAN = 2; - @PublicApi(exclude = true) + @Hide public static final int STATUS_NO_INFO_IN_DATABASE = 3; - @PublicApi(exclude = true) + @Hide public static final int STATUS_INVALID_SCAN = 4; - @PublicApi(exclude = true) + @Hide public static final int STATUS_UNABLE_TO_QUERY_DATABASE = 5; - @PublicApi(exclude = true) + @Hide public static final int STATUS_SCANS_DISABLED_IN_SETTINGS = 6; - @PublicApi(exclude = true) + @Hide public static final int STATUS_LOCATION_DISABLED_IN_SETTINGS = 7; - @PublicApi(exclude = true) + @Hide public static final int STATUS_IN_PROGRESS = 8; - @SafeParceled(1000) + @Field(1000) private int versionCode = 2; - @SafeParceled(1) - @PublicApi(exclude = true) + @Field(1) + @Hide public int cellStatus; - @SafeParceled(2) - @PublicApi(exclude = true) + @Field(2) + @Hide public int wifiStatus; - @SafeParceled(3) - @PublicApi(exclude = true) + @Field(3) + @Hide public long elapsedRealtimeNs; - @SafeParceled(4) - @PublicApi(exclude = true) + @Field(4) + @Hide public int locationStatus; - @PublicApi(exclude = true) - public LocationAvailability() { + @Field(5) + @Hide + public NetworkLocationStatus[] batchedStatus; + + private LocationAvailability() { + } + + @Hide + public LocationAvailability(int locationStatus, int cellStatus, int wifiStatus, long elapsedRealtimeNs, NetworkLocationStatus[] batchedStatus) { + this.locationStatus = locationStatus; + this.cellStatus = cellStatus; + this.wifiStatus = wifiStatus; + this.elapsedRealtimeNs = elapsedRealtimeNs; + this.batchedStatus = batchedStatus; } @Override @@ -88,9 +94,6 @@ public boolean equals(Object o) { /** * Extracts the {@link LocationAvailability} from an Intent. - *

- * This is a utility function which extracts the {@link LocationAvailability} from the extras - * of an Intent that was sent in response to a location request. * * @return a {@link LocationAvailability}, or null if the Intent doesn't contain this data. */ @@ -103,9 +106,6 @@ public static LocationAvailability extractLocationAvailability(Intent intent) { /** * Returns true if an Intent contains a {@link LocationAvailability}. - *

- * This is a utility function that can be called from inside an intent receiver to make sure the - * received intent contains location availability data. * * @return true if the intent contains a {@link LocationAvailability}, false otherwise. */ @@ -119,10 +119,7 @@ public int hashCode() { } /** - * Returns true if the device location is known and reasonably up to date within the hints - * requested by the active {@link LocationRequest}s. Failure to determine location may result - * from a number of causes including disabled location settings or an inability to retrieve - * sensor data in the device's environment. + * Returns true if the device location is generally available. */ public boolean isLocationAvailable() { return locationStatus < 1000; @@ -130,7 +127,7 @@ public boolean isLocationAvailable() { @Override public String toString() { - return "LocationAvailability[isLocationAvailable: " + isLocationAvailable() + "]"; + return "LocationAvailability[" + isLocationAvailable() + "]"; } public static final Creator CREATOR = new AutoCreator(LocationAvailability.class); diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationAvailabilityRequest.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationAvailabilityRequest.java new file mode 100644 index 0000000000..02ecff8340 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationAvailabilityRequest.java @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.location; + +import androidx.annotation.Nullable; +import com.google.android.gms.location.internal.ClientIdentity; +import org.microg.gms.common.Hide; +import org.microg.safeparcel.AutoSafeParcelable; + +@Hide +public class LocationAvailabilityRequest extends AutoSafeParcelable { + @Field(1) + public boolean bypass; + @Field(2) + @Nullable + public ClientIdentity impersonation; + public static final Creator CREATOR = new AutoCreator<>(LocationAvailabilityRequest.class); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationClient.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationClient.java index 47ec956852..2e317262a9 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/LocationClient.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationClient.java @@ -21,18 +21,24 @@ import android.location.Location; import android.os.Looper; +import androidx.annotation.RequiresPermission; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; import org.microg.gms.common.ForwardConnectionCallbacks; import org.microg.gms.common.ForwardConnectionFailedListener; +import org.microg.gms.common.Hide; import org.microg.gms.common.api.AbstractPlayServicesClient; +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; + /** * This class is deprecated as of play services 6.5, do not use it in production systems, * it's just a forwarder for the {@link FusedLocationProviderApi}. */ @Deprecated +@Hide public class LocationClient extends AbstractPlayServicesClient { public static final String KEY_LOCATION_CHANGED = "com.google.android.location.LOCATION"; @@ -46,11 +52,13 @@ public LocationClient(Context context, ConnectionCallbacks callbacks, .build()); } + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) public Location getLastLocation() { assertConnected(); return LocationServices.FusedLocationApi.getLastLocation(googleApiClient); } + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) public void requestLocationUpdates(LocationRequest request, LocationListener listener) { assertConnected(); @@ -58,6 +66,7 @@ public void requestLocationUpdates(LocationRequest request, listener).await(); } + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) public void requestLocationUpdates(LocationRequest request, LocationListener listener, Looper looper) { assertConnected(); @@ -65,6 +74,7 @@ public void requestLocationUpdates(LocationRequest request, listener, looper).await(); } + @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) public void requestLocationUpdates(LocationRequest request, PendingIntent callbackIntent) { assertConnected(); diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationListener.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationListener.java index 59c9585cf2..ae8fd04709 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/LocationListener.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationListener.java @@ -17,6 +17,7 @@ package com.google.android.gms.location; import android.location.Location; +import androidx.annotation.NonNull; /** * Used for receiving notifications from the {@link FusedLocationProviderApi} when the location has @@ -30,5 +31,5 @@ public interface LocationListener { * * @param location The updated location. */ - public void onLocationChanged(Location location); + public void onLocationChanged(@NonNull Location location); } diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationRequest.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationRequest.java index c9805162ce..a2d5e076e8 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/LocationRequest.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationRequest.java @@ -15,7 +15,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresPermission; -import com.google.android.gms.common.internal.ClientIdentity; +import com.google.android.gms.location.internal.ClientIdentity; import org.microg.gms.common.PublicApi; import org.microg.gms.location.GranularityUtil; import org.microg.gms.location.PriorityUtil; diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsConfiguration.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsConfiguration.java index 29dabea9dc..5f5a3d59e1 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsConfiguration.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsConfiguration.java @@ -5,8 +5,10 @@ package com.google.android.gms.location; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; +@Hide public class LocationSettingsConfiguration extends AutoSafeParcelable { @Field(1) public String justificationText; diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsResponse.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsResponse.java new file mode 100644 index 0000000000..60da9d3e23 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsResponse.java @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResolvableApiException; +import com.google.android.gms.common.api.Response; +import com.google.android.gms.tasks.Task; +import org.microg.gms.common.PublicApi; + +/** + * Successful response of checking settings via {@link SettingsApi#checkLocationSettings(GoogleApiClient, LocationSettingsRequest)}. + *

+ * If a {@link Task} with this response type fails, it will receive a {@link ResolvableApiException} which may be able to resolve the failure. + * See {@link SettingsClient} for more details. + *

+ * The current location settings states can be accessed via {@link #getLocationSettingsStates()}. See {@link LocationSettingsResult} for more details. + */ +public class LocationSettingsResponse extends Response { + /** + * Retrieves the location settings states. + */ + @Nullable + public LocationSettingsStates getLocationSettingsStates() { + return getResult().getLocationSettingsStates(); + } + + @PublicApi(exclude = true) + public LocationSettingsResponse(@NonNull LocationSettingsResult result) { + super(result); + } +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationStatus.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationStatus.java index 6cd2060a08..087842526c 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/LocationStatus.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationStatus.java @@ -16,11 +16,13 @@ package com.google.android.gms.location; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; import java.util.Arrays; +@Hide public class LocationStatus extends AutoSafeParcelable { public static final int STATUS_SUCCESSFUL = 0; public static final int STATUS_UNKNOWN = 1; diff --git a/play-services-location/src/main/java/com/google/android/gms/location/NetworkLocationStatus.java b/play-services-location/src/main/java/com/google/android/gms/location/NetworkLocationStatus.java new file mode 100644 index 0000000000..d577fadc5e --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/NetworkLocationStatus.java @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.location; + +import org.microg.gms.common.Hide; +import org.microg.safeparcel.AutoSafeParcelable; + +@Hide +public class NetworkLocationStatus extends AutoSafeParcelable { + @Field(1) + public int wifiStatus; + @Field(2) + public int cellStatus; + @Field(3) + public long systemTimeMs; + @Field(4) + public long elapsedRealtimeNs; + + public NetworkLocationStatus() { + } + + public NetworkLocationStatus(int wifiStatus, int cellStatus, long systemTimeMs, long elapsedRealtimeNs) { + this.wifiStatus = wifiStatus; + this.cellStatus = cellStatus; + this.systemTimeMs = systemTimeMs; + this.elapsedRealtimeNs = elapsedRealtimeNs; + } + + public static final Creator CREATOR = new AutoCreator<>(NetworkLocationStatus.class); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/SettingsApi.java b/play-services-location/src/main/java/com/google/android/gms/location/SettingsApi.java index 42aa5a794b..a0497bbb4d 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/SettingsApi.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/SettingsApi.java @@ -16,6 +16,8 @@ *

* This API makes it easy for an app to ensure that the device's system settings are properly * configured for the app's location needs. + * + * @deprecated Use GoogleApi-based API {@link SettingsClient} instead. */ @Deprecated public interface SettingsApi { diff --git a/play-services-location/src/main/java/com/google/android/gms/location/ThrottleBehavior.java b/play-services-location/src/main/java/com/google/android/gms/location/ThrottleBehavior.java index d1dc300d75..3b1a0cf314 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/ThrottleBehavior.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/ThrottleBehavior.java @@ -6,6 +6,7 @@ package com.google.android.gms.location; import androidx.annotation.IntDef; +import org.microg.gms.common.Hide; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -15,6 +16,7 @@ @Target({ElementType.TYPE_USE}) @Retention(RetentionPolicy.SOURCE) @IntDef({ThrottleBehavior.THROTTLE_BACKGROUND, ThrottleBehavior.THROTTLE_ALWAYS, ThrottleBehavior.THROTTLE_NEVER}) +@Hide public @interface ThrottleBehavior { int THROTTLE_BACKGROUND = 0; int THROTTLE_ALWAYS = 1; diff --git a/play-services-location/src/main/java/com/google/android/gms/location/internal/ClientIdentity.java b/play-services-location/src/main/java/com/google/android/gms/location/internal/ClientIdentity.java new file mode 100644 index 0000000000..da184ab73b --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/internal/ClientIdentity.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.location.internal; + +import androidx.annotation.Nullable; +import com.google.android.gms.common.Feature; +import org.microg.gms.common.Hide; +import org.microg.safeparcel.AutoSafeParcelable; + +import java.util.Collections; +import java.util.List; + +@Hide +public class ClientIdentity extends AutoSafeParcelable { + @Field(1) + public int uid; + @Field(2) + public int pid; + @Field(3) + public String packageName; + @Field(4) + @Nullable + public String attributionTag; + @Field(5) + public int clientSdkVersion; + @Field(6) + @Nullable + public String listenerId; + @Field(7) + @Nullable + public ClientIdentity impersonator; + @Field(8) + public List clientFeatures = Collections.emptyList(); + + private ClientIdentity() {} + + public ClientIdentity(String packageName) { + this.packageName = packageName; + } + + public static final Creator CREATOR = new AutoCreator<>(ClientIdentity.class); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/internal/DeviceOrientationRequestInternal.java b/play-services-location/src/main/java/com/google/android/gms/location/internal/DeviceOrientationRequestInternal.java index 03aeb5db2e..b60ee1b159 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/internal/DeviceOrientationRequestInternal.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/internal/DeviceOrientationRequestInternal.java @@ -8,10 +8,12 @@ import com.google.android.gms.common.internal.ClientIdentity; import com.google.android.gms.location.DeviceOrientationRequest; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; import java.util.List; +@Hide public class DeviceOrientationRequestInternal extends AutoSafeParcelable { @Field(1) diff --git a/play-services-location/src/main/java/com/google/android/gms/location/internal/DeviceOrientationRequestUpdateData.java b/play-services-location/src/main/java/com/google/android/gms/location/internal/DeviceOrientationRequestUpdateData.java index 5a2e759ebb..925a8efb4c 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/internal/DeviceOrientationRequestUpdateData.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/internal/DeviceOrientationRequestUpdateData.java @@ -7,8 +7,10 @@ import com.google.android.gms.location.IDeviceOrientationListener; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; +@Hide public class DeviceOrientationRequestUpdateData extends AutoSafeParcelable { public static final int REQUEST_UPDATES = 1; public static final int REMOVE_UPDATES = 2; diff --git a/play-services-location/src/main/java/com/google/android/gms/location/internal/FusedLocationProviderResult.java b/play-services-location/src/main/java/com/google/android/gms/location/internal/FusedLocationProviderResult.java index b4b6516da1..82d5ca2776 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/internal/FusedLocationProviderResult.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/internal/FusedLocationProviderResult.java @@ -18,9 +18,11 @@ import com.google.android.gms.common.api.Status; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; +@Hide public class FusedLocationProviderResult extends AutoSafeParcelable { public static final FusedLocationProviderResult SUCCESS = FusedLocationProviderResult.create(Status.SUCCESS); diff --git a/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationReceiver.java b/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationReceiver.java new file mode 100644 index 0000000000..ecfe54b59c --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationReceiver.java @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.location.internal; + +import android.app.PendingIntent; +import android.os.IBinder; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.location.ILocationCallback; +import com.google.android.gms.location.ILocationListener; +import org.microg.gms.common.Hide; +import org.microg.safeparcel.AutoSafeParcelable; + +@Hide +public class LocationReceiver extends AutoSafeParcelable { + public static final int TYPE_LISTENER = 1; + public static final int TYPE_CALLBACK = 2; + public static final int TYPE_PENDING_INTENT = 3; + public static final int TYPE_STATUS_CALLBACK = 4; + public static final int TYPE_AVAILABILITY_STATUS_CALLBACK = 5; + + @Field(1) + public int type; + @Field(2) + @Nullable + public IBinder oldBinderReceiver; + @Field(3) + @Nullable + public IBinder binderReceiver; + @Field(4) + @Nullable + public PendingIntent pendingIntentReceiver; + @Field(5) + @Nullable + public String moduleId; + @Field(6) + @Nullable + public String listenerId; + + private LocationReceiver() { + } + + public LocationReceiver(@NonNull ILocationListener listener) { + this(listener, null); + } + + public LocationReceiver(@NonNull ILocationListener listener, @Nullable String listenerId) { + type = TYPE_LISTENER; + binderReceiver = listener.asBinder(); + this.listenerId = listenerId; + } + + public LocationReceiver(@NonNull ILocationCallback callback) { + this(callback, null); + } + + public LocationReceiver(@NonNull ILocationCallback callback, @Nullable String listenerId) { + type = TYPE_CALLBACK; + binderReceiver = callback.asBinder(); + this.listenerId = listenerId; + } + + public LocationReceiver(@NonNull PendingIntent pendingIntent) { + this(pendingIntent, null); + } + + public LocationReceiver(@NonNull PendingIntent pendingIntent, @Nullable String listenerId) { + type = TYPE_PENDING_INTENT; + pendingIntentReceiver = pendingIntent; + this.listenerId = listenerId; + } + + public LocationReceiver(@NonNull ILocationStatusCallback callback) { + this(callback, null); + } + + public LocationReceiver(@NonNull ILocationStatusCallback callback, @Nullable String listenerId) { + type = TYPE_STATUS_CALLBACK; + binderReceiver = callback.asBinder(); + this.listenerId = listenerId; + } + + public LocationReceiver(@NonNull ILocationAvailabilityStatusCallback callback) { + type = TYPE_AVAILABILITY_STATUS_CALLBACK; + binderReceiver = callback.asBinder(); + } + + public ILocationListener getListener() { + if (type != TYPE_LISTENER) throw new IllegalStateException(); + return ILocationListener.Stub.asInterface(binderReceiver); + } + + public ILocationCallback getCallback() { + if (type != TYPE_CALLBACK) throw new IllegalStateException(); + return ILocationCallback.Stub.asInterface(binderReceiver); + } + + public ILocationStatusCallback getStatusCallback() { + if (type != TYPE_STATUS_CALLBACK) throw new IllegalStateException(); + return ILocationStatusCallback.Stub.asInterface(binderReceiver); + } + + public ILocationAvailabilityStatusCallback getAvailabilityStatusCallback() { + if (type != TYPE_AVAILABILITY_STATUS_CALLBACK) throw new IllegalStateException(); + return ILocationAvailabilityStatusCallback.Stub.asInterface(binderReceiver); + } + + public static final Creator CREATOR = new AutoCreator<>(LocationReceiver.class); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationRequestInternal.java b/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationRequestInternal.java index a50a038dc1..d4a3e16841 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationRequestInternal.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationRequestInternal.java @@ -12,18 +12,20 @@ import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.ThrottleBehavior; +import org.microg.gms.common.Hide; import org.microg.gms.utils.WorkSourceUtil; import org.microg.safeparcel.AutoSafeParcelable; import java.util.List; +@Hide public class LocationRequestInternal extends AutoSafeParcelable { @Field(1000) private int versionCode = 1; @Field(1) - public LocationRequest request; + private LocationRequest request; @Field(2) @Deprecated @@ -96,6 +98,13 @@ public String toString() { '}'; } + public LocationRequestInternal() { + } + + public LocationRequestInternal(LocationRequest request) { + this.request = request; + } + @SuppressLint("MissingPermission") public LocationRequest getRequest() { LocationRequest.Builder builder = new LocationRequest.Builder(this.request); diff --git a/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationRequestUpdateData.java b/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationRequestUpdateData.java index 0d66e32eda..8c0eb97912 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationRequestUpdateData.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/internal/LocationRequestUpdateData.java @@ -10,35 +10,40 @@ import com.google.android.gms.location.ILocationCallback; import com.google.android.gms.location.ILocationListener; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; +@Hide public class LocationRequestUpdateData extends AutoSafeParcelable { public static final int REQUEST_UPDATES = 1; public static final int REMOVE_UPDATES = 2; - @SafeParceled(1000) + @Field(1000) private int versionCode; - @SafeParceled(1) + @Field(1) public int opCode; - @SafeParceled(2) + @Field(2) public LocationRequestInternal request; - @SafeParceled(3) + @Field(3) public ILocationListener listener; - @SafeParceled(4) + @Field(4) public PendingIntent pendingIntent; - @SafeParceled(5) + @Field(5) public ILocationCallback callback; - @SafeParceled(6) + @Field(6) public IFusedLocationProviderCallback fusedLocationProviderCallback; + @Field(8) + public String listenerId; + @Override public String toString() { return "LocationRequestUpdateData{" + diff --git a/play-services-location/src/main/java/com/google/android/gms/location/internal/ParcelableGeofence.java b/play-services-location/src/main/java/com/google/android/gms/location/internal/ParcelableGeofence.java index 3abc611c22..00751fb8ea 100644 --- a/play-services-location/src/main/java/com/google/android/gms/location/internal/ParcelableGeofence.java +++ b/play-services-location/src/main/java/com/google/android/gms/location/internal/ParcelableGeofence.java @@ -1,69 +1,61 @@ /* - * Copyright (C) 2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: 2017 microG Project Team + * SPDX-License-Identifier: Apache-2.0 */ package com.google.android.gms.location.internal; import com.google.android.gms.location.Geofence; +import org.microg.gms.common.Hide; import org.microg.safeparcel.AutoSafeParcelable; -import org.microg.safeparcel.SafeParceled; +@Hide public class ParcelableGeofence extends AutoSafeParcelable implements Geofence { - @SafeParceled(1000) + @Field(1000) private int versionCode = 1; - @SafeParceled(1) + @Field(1) public String requestId; - @SafeParceled(2) + @Field(2) public long expirationTime; - @SafeParceled(3) + @Field(3) public int regionType; - @SafeParceled(4) + @Field(4) public double latitude; - @SafeParceled(5) + @Field(5) public double longitude; - @SafeParceled(6) + @Field(6) public float radius; - @SafeParceled(7) - public int transitionType; + @Field(7) + public @TransitionTypes int transitionTypes; - @SafeParceled(8) + @Field(8) public int notificationResponsiveness; - @SafeParceled(9) + @Field(9) public int loiteringDelay; private ParcelableGeofence() { + notificationResponsiveness = 0; + loiteringDelay = -1; } - public ParcelableGeofence(String requestId, long expirationTime, int regionType, double latitude, double longitude, float radius, int transitionType, int notificationResponsiveness, int loiteringDelay) { + public ParcelableGeofence(String requestId, long expirationTime, int regionType, double latitude, double longitude, float radius, @TransitionTypes int transitionTypes, int notificationResponsiveness, int loiteringDelay) { this.requestId = requestId; this.expirationTime = expirationTime; this.regionType = regionType; this.latitude = latitude; this.longitude = longitude; this.radius = radius; - this.transitionType = transitionType; + this.transitionTypes = transitionTypes; this.notificationResponsiveness = notificationResponsiveness; this.loiteringDelay = loiteringDelay; } @@ -73,5 +65,40 @@ public String getRequestId() { return requestId; } + @Override + public long getExpirationTime() { + return expirationTime; + } + + @Override + public double getLatitude() { + return latitude; + } + + @Override + public double getLongitude() { + return longitude; + } + + @Override + public float getRadius() { + return radius; + } + + @Override + public @TransitionTypes int getTransitionTypes() { + return transitionTypes; + } + + @Override + public int getNotificationResponsiveness() { + return notificationResponsiveness; + } + + @Override + public int getLoiteringDelay() { + return loiteringDelay; + } + public static final Creator CREATOR = new AutoCreator(ParcelableGeofence.class); } diff --git a/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java b/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java index 2ae7a1d0f1..e858341825 100644 --- a/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java +++ b/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java @@ -10,10 +10,9 @@ import android.location.Location; import android.os.Looper; -import com.google.android.gms.location.FusedLocationProviderClient; -import com.google.android.gms.location.LocationCallback; -import com.google.android.gms.location.LocationListener; -import com.google.android.gms.location.LocationRequest; +import androidx.annotation.NonNull; +import com.google.android.gms.location.*; +import com.google.android.gms.tasks.CancellationToken; import com.google.android.gms.tasks.Task; import org.microg.gms.common.api.ReturningGoogleApiCall; @@ -31,10 +30,34 @@ public Task flushLocations() { return scheduleTask((ReturningGoogleApiCall) (client) -> null); } + @NonNull + @Override + public Task getCurrentLocation(int priority, CancellationToken cancellationToken) { + return null; + } + + @NonNull + @Override + public Task getCurrentLocation(CurrentLocationRequest request, CancellationToken cancellationToken) { + return null; + } + + @NonNull + @Override + public Task getLastLocation(LastLocationRequest request) { + return null; + } + public Task getLastLocation() { return scheduleTask((ReturningGoogleApiCall) LocationClientImpl::getLastLocation); } + @NonNull + @Override + public Task getLocationAvailability() { + return null; + } + @Override public Task removeLocationUpdates(LocationListener listener) { return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.removeLocationUpdates(listener)); @@ -70,6 +93,16 @@ public Task requestLocationUpdates(LocationRequest request, PendingIntent return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.requestLocationUpdates(request, pendingIntent)); } + @Override + public Task setMockLocation(Location location) { + return null; + } + + @Override + public Task setMockMode(boolean mockMode) { + return null; + } + @Override public Task removeLocationUpdates(LocationCallback callback) { return scheduleTask((VoidReturningGoogleApiCall) (client) -> client.removeLocationUpdates(callback)); diff --git a/play-services-location/src/main/java/org/microg/gms/location/GeofencingClientImpl.java b/play-services-location/src/main/java/org/microg/gms/location/GeofencingClientImpl.java index 9ec23067e2..4d6f4dba40 100644 --- a/play-services-location/src/main/java/org/microg/gms/location/GeofencingClientImpl.java +++ b/play-services-location/src/main/java/org/microg/gms/location/GeofencingClientImpl.java @@ -5,15 +5,39 @@ package org.microg.gms.location; +import android.app.PendingIntent; import android.content.Context; +import androidx.annotation.NonNull; import com.google.android.gms.common.api.Api; import com.google.android.gms.common.api.GoogleApi; import com.google.android.gms.location.GeofencingClient; +import com.google.android.gms.location.GeofencingRequest; import com.google.android.gms.location.LocationServices; +import com.google.android.gms.tasks.Task; + +import java.util.List; public class GeofencingClientImpl extends GoogleApi implements GeofencingClient { public GeofencingClientImpl(Context context) { super(context, LocationServices.API); } + + @NonNull + @Override + public Task addGeofences(GeofencingRequest geofencingRequest, PendingIntent pendingIntent) { + return null; + } + + @NonNull + @Override + public Task removeGeofences(List geofenceRequestIds) { + return null; + } + + @NonNull + @Override + public Task removeGeofences(PendingIntent pendingIntent) { + return null; + } } diff --git a/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java b/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java index d63cf8a66d..7b4b62ddf2 100644 --- a/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java +++ b/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java @@ -98,6 +98,11 @@ public void requestLocationUpdates(LocationRequest request, final LocationListen public void onLocationChanged(Location location) throws RemoteException { listener.onLocationChanged(location); } + + @Override + public void cancel() throws RemoteException { + + } }); } getServiceInterface().requestLocationUpdatesWithPackage(request, listenerMap.get(listener), getContext().getPackageName()); @@ -120,6 +125,11 @@ public void requestLocationUpdates(LocationRequest request, Executor executor, L public void onLocationChanged(Location location) throws RemoteException { executor.execute(() -> listener.onLocationChanged(location)); } + + @Override + public void cancel() throws RemoteException { + + } }); } getServiceInterface().requestLocationUpdatesWithPackage(request, listenerMap.get(listener), getContext().getPackageName()); @@ -137,6 +147,11 @@ public void requestLocationUpdates(LocationRequest request, Executor executor, L public void onLocationChanged(Location location) throws RemoteException { executor.execute(() -> callback.onLocationResult(LocationResult.create(Collections.singletonList(location)))); } + + @Override + public void cancel() throws RemoteException { + + } }); } getServiceInterface().requestLocationUpdatesWithPackage(request, callbackMap.get(callback), getContext().getPackageName()); diff --git a/play-services-location/system-api/build.gradle b/play-services-location/system-api/build.gradle new file mode 100644 index 0000000000..1e7305574e --- /dev/null +++ b/play-services-location/system-api/build.gradle @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' + +dependencies { + implementation "androidx.annotation:annotation:$annotationVersion" +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } +} \ No newline at end of file diff --git a/play-services-location/system-api/src/main/AndroidManifest.xml b/play-services-location/system-api/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..670be7c934 --- /dev/null +++ b/play-services-location/system-api/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/play-services-location/system-api/src/main/java/android/location/GeocoderParams.java b/play-services-location/system-api/src/main/java/android/location/GeocoderParams.java new file mode 100644 index 0000000000..26e5ceb3d8 --- /dev/null +++ b/play-services-location/system-api/src/main/java/android/location/GeocoderParams.java @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2010, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.location; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.Locale; + +/** + * This class contains extra parameters to pass to an IGeocodeProvider + * implementation from the Geocoder class. Currently this contains the + * language, country and variant information from the Geocoder's locale + * as well as the Geocoder client's package name for geocoder server + * logging. This information is kept in a separate class to allow for + * future expansion of the IGeocodeProvider interface. + * + * @hide + */ +public class GeocoderParams implements Parcelable { + /** + * This object is only constructed by the Geocoder class + * + * @hide + */ + public GeocoderParams(Context context, Locale locale) { + } + + /** + * Returns the client UID. + */ + @RequiresApi(33) + public int getClientUid() { + return 0; + } + + /** + * returns the package name of the Geocoder's client + */ + public String getClientPackage() { + return null; + } + + /** + * Returns the client attribution tag. + */ + @RequiresApi(33) + public @Nullable String getClientAttributionTag() { + return null; + } + + /** + * returns the Geocoder's locale + */ + public Locale getLocale() { + return null; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public GeocoderParams createFromParcel(Parcel in) { + return null; + } + + public GeocoderParams[] newArray(int size) { + return null; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + } +} diff --git a/play-services-location/system-api/src/main/java/android/location/Geofence.java b/play-services-location/system-api/src/main/java/android/location/Geofence.java new file mode 100644 index 0000000000..68f1b2d0c8 --- /dev/null +++ b/play-services-location/system-api/src/main/java/android/location/Geofence.java @@ -0,0 +1,172 @@ +/* + * SPDX-FileCopyrightText: 2012, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.location; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents a geographical boundary, also known as a geofence. + * + *

Currently only circular geofences are supported and they do not support altitude changes. + * + * @hide + */ +public final class Geofence implements Parcelable { + /** @hide */ + public static final int TYPE_HORIZONTAL_CIRCLE = 1; + + private final int mType; + private final double mLatitude; + private final double mLongitude; + private final float mRadius; + + /** + * Create a circular geofence (on a flat, horizontal plane). + * + * @param latitude latitude in degrees, between -90 and +90 inclusive + * @param longitude longitude in degrees, between -180 and +180 inclusive + * @param radius radius in meters + * @return a new geofence + * @throws IllegalArgumentException if any parameters are out of range + */ + public static Geofence createCircle(double latitude, double longitude, float radius) { + return new Geofence(latitude, longitude, radius); + } + + private Geofence(double latitude, double longitude, float radius) { + checkRadius(radius); + checkLatLong(latitude, longitude); + mType = TYPE_HORIZONTAL_CIRCLE; + mLatitude = latitude; + mLongitude = longitude; + mRadius = radius; + } + + /** @hide */ + public int getType() { + return mType; + } + + /** @hide */ + public double getLatitude() { + return mLatitude; + } + + /** @hide */ + public double getLongitude() { + return mLongitude; + } + + /** @hide */ + public float getRadius() { + return mRadius; + } + + private static void checkRadius(float radius) { + if (radius <= 0) { + throw new IllegalArgumentException("invalid radius: " + radius); + } + } + + private static void checkLatLong(double latitude, double longitude) { + if (latitude > 90.0 || latitude < -90.0) { + throw new IllegalArgumentException("invalid latitude: " + latitude); + } + if (longitude > 180.0 || longitude < -180.0) { + throw new IllegalArgumentException("invalid longitude: " + longitude); + } + } + + private static void checkType(int type) { + if (type != TYPE_HORIZONTAL_CIRCLE) { + throw new IllegalArgumentException("invalid type: " + type); + } + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public Geofence createFromParcel(Parcel in) { + int type = in.readInt(); + double latitude = in.readDouble(); + double longitude = in.readDouble(); + float radius = in.readFloat(); + checkType(type); + return Geofence.createCircle(latitude, longitude, radius); + } + @Override + public Geofence[] newArray(int size) { + return new Geofence[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mType); + parcel.writeDouble(mLatitude); + parcel.writeDouble(mLongitude); + parcel.writeFloat(mRadius); + } + + private static String typeToString(int type) { + switch (type) { + case TYPE_HORIZONTAL_CIRCLE: + return "CIRCLE"; + default: + checkType(type); + return null; + } + } + + @Override + public String toString() { + return String.format("Geofence[%s %.6f, %.6f %.0fm]", + typeToString(mType), mLatitude, mLongitude, mRadius); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(mLatitude); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(mLongitude); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + Float.floatToIntBits(mRadius); + result = prime * result + mType; + return result; + } + + /** + * Two geofences are equal if they have identical properties. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof Geofence)) + return false; + Geofence other = (Geofence) obj; + if (mRadius != other.mRadius) + return false; + if (mLatitude != other.mLatitude) + return false; + if (mLongitude != other.mLongitude) + return false; + if (mType != other.mType) + return false; + return true; + } +} diff --git a/play-services-location/system-api/src/main/java/android/location/Location.java b/play-services-location/system-api/src/main/java/android/location/Location.java new file mode 100644 index 0000000000..facb1c8582 --- /dev/null +++ b/play-services-location/system-api/src/main/java/android/location/Location.java @@ -0,0 +1,555 @@ +/* + * SPDX-FileCopyrightText: 2007, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.location; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Printer; + +/** + * A data class representing a geographic location. + *

+ *

A location can consist of a latitude, longitude, timestamp, + * and other information such as bearing, altitude and velocity. + *

+ *

All locations generated by the {@link LocationManager} are + * guaranteed to have a valid latitude, longitude, and timestamp + * (both UTC time and elapsed real-time since boot), all other + * parameters are optional. + */ +public class Location implements Parcelable { + /** + * Constant used to specify formatting of a latitude or longitude + * in the form "[+-]DDD.DDDDD where D indicates degrees. + */ + public static final int FORMAT_DEGREES = 0; + + /** + * Constant used to specify formatting of a latitude or longitude + * in the form "[+-]DDD:MM.MMMMM" where D indicates degrees and + * M indicates minutes of arc (1 minute = 1/60th of a degree). + */ + public static final int FORMAT_MINUTES = 1; + + /** + * Constant used to specify formatting of a latitude or longitude + * in the form "DDD:MM:SS.SSSSS" where D indicates degrees, M + * indicates minutes of arc, and S indicates seconds of arc (1 + * minute = 1/60th of a degree, 1 second = 1/3600th of a degree). + */ + public static final int FORMAT_SECONDS = 2; + + /** + * Bundle key for a version of the location that has been fed through + * LocationFudger. Allows location providers to flag locations as being + * safe for use with ACCESS_COARSE_LOCATION permission. + * + * @hide + */ + public static final String EXTRA_COARSE_LOCATION = "coarseLocation"; + + /** + * Bundle key for a version of the location containing no GPS data. + * Allows location providers to flag locations as being safe to + * feed to LocationFudger. + * + * @hide + */ + public static final String EXTRA_NO_GPS_LOCATION = "noGPSLocation"; + + /** + * Construct a new Location with a named provider. + *

+ *

By default time, latitude and longitude are 0, and the location + * has no bearing, altitude, speed, accuracy or extras. + * + * @param provider the name of the provider that generated this location + */ + public Location(String provider) { + } + + /** + * Construct a new Location object that is copied from an existing one. + */ + public Location(Location l) { + } + + /** + * Sets the contents of the location to the values from the given location. + */ + public void set(Location l) { + } + + /** + * Clears the contents of the location. + */ + public void reset() { + } + + /** + * Converts a coordinate to a String representation. The outputType + * may be one of FORMAT_DEGREES, FORMAT_MINUTES, or FORMAT_SECONDS. + * The coordinate must be a valid double between -180.0 and 180.0. + * + * @throws IllegalArgumentException if coordinate is less than + * -180.0, greater than 180.0, or is not a number. + * @throws IllegalArgumentException if outputType is not one of + * FORMAT_DEGREES, FORMAT_MINUTES, or FORMAT_SECONDS. + */ + public static String convert(double coordinate, int outputType) { + return null; + } + + /** + * Converts a String in one of the formats described by + * FORMAT_DEGREES, FORMAT_MINUTES, or FORMAT_SECONDS into a + * double. + * + * @throws NullPointerException if coordinate is null + * @throws IllegalArgumentException if the coordinate is not + * in one of the valid formats. + */ + public static double convert(String coordinate) { + return 0; + } + + private static void computeDistanceAndBearing(double lat1, double lon1, + double lat2, double lon2, float[] results) { + } + + /** + * Computes the approximate distance in meters between two + * locations, and optionally the initial and final bearings of the + * shortest path between them. Distance and bearing are defined using the + * WGS84 ellipsoid. + *

+ *

The computed distance is stored in results[0]. If results has length + * 2 or greater, the initial bearing is stored in results[1]. If results has + * length 3 or greater, the final bearing is stored in results[2]. + * + * @param startLatitude the starting latitude + * @param startLongitude the starting longitude + * @param endLatitude the ending latitude + * @param endLongitude the ending longitude + * @param results an array of floats to hold the results + * @throws IllegalArgumentException if results is null or has length < 1 + */ + public static void distanceBetween(double startLatitude, double startLongitude, + double endLatitude, double endLongitude, float[] results) { + } + + /** + * Returns the approximate distance in meters between this + * location and the given location. Distance is defined using + * the WGS84 ellipsoid. + * + * @param dest the destination location + * @return the approximate distance in meters + */ + public float distanceTo(Location dest) { + return 0; + } + + /** + * Returns the approximate initial bearing in degrees East of true + * North when traveling along the shortest path between this + * location and the given location. The shortest path is defined + * using the WGS84 ellipsoid. Locations that are (nearly) + * antipodal may produce meaningless results. + * + * @param dest the destination location + * @return the initial bearing in degrees + */ + public float bearingTo(Location dest) { + return 0; + } + + /** + * Returns the name of the provider that generated this fix. + * + * @return the provider, or null if it has not been set + */ + public String getProvider() { + return null; + } + + /** + * Sets the name of the provider that generated this fix. + */ + public void setProvider(String provider) { + } + + /** + * Return the UTC time of this fix, in milliseconds since January 1, 1970. + *

+ *

Note that the UTC time on a device is not monotonic: it + * can jump forwards or backwards unpredictably. So always use + * {@link #getElapsedRealtimeNanos} when calculating time deltas. + *

+ *

On the other hand, {@link #getTime} is useful for presenting + * a human readable time to the user, or for carefully comparing + * location fixes across reboot or across devices. + *

+ *

All locations generated by the {@link LocationManager} + * are guaranteed to have a valid UTC time, however remember that + * the system time may have changed since the location was generated. + * + * @return time of fix, in milliseconds since January 1, 1970. + */ + public long getTime() { + return 0; + } + + /** + * Set the UTC time of this fix, in milliseconds since January 1, + * 1970. + * + * @param time UTC time of this fix, in milliseconds since January 1, 1970 + */ + public void setTime(long time) { + } + + /** + * Return the time of this fix, in elapsed real-time since system boot. + *

+ *

This value can be reliably compared to + * {@link android.os.SystemClock#elapsedRealtimeNanos}, + * to calculate the age of a fix and to compare Location fixes. This + * is reliable because elapsed real-time is guaranteed monotonic for + * each system boot and continues to increment even when the system + * is in deep sleep (unlike {@link #getTime}. + *

+ *

All locations generated by the {@link LocationManager} + * are guaranteed to have a valid elapsed real-time. + * + * @return elapsed real-time of fix, in nanoseconds since system boot. + */ + public long getElapsedRealtimeNanos() { + return 0; + } + + /** + * Set the time of this fix, in elapsed real-time since system boot. + * + * @param time elapsed real-time of fix, in nanoseconds since system boot. + */ + public void setElapsedRealtimeNanos(long time) { + } + + /** + * Get the latitude, in degrees. + *

+ *

All locations generated by the {@link LocationManager} + * will have a valid latitude. + */ + public double getLatitude() { + return 0; + } + + /** + * Set the latitude, in degrees. + */ + public void setLatitude(double latitude) { + } + + /** + * Get the longitude, in degrees. + *

+ *

All locations generated by the {@link LocationManager} + * will have a valid longitude. + */ + public double getLongitude() { + return 0; + } + + /** + * Set the longitude, in degrees. + */ + public void setLongitude(double longitude) { + } + + /** + * True if this location has an altitude. + */ + public boolean hasAltitude() { + return false; + } + + /** + * Get the altitude if available, in meters above the WGS 84 reference + * ellipsoid. + *

+ *

If this location does not have an altitude then 0.0 is returned. + */ + public double getAltitude() { + return 0; + } + + /** + * Set the altitude, in meters above the WGS 84 reference ellipsoid. + *

+ *

Following this call {@link #hasAltitude} will return true. + */ + public void setAltitude(double altitude) { + } + + /** + * Remove the altitude from this location. + *

+ *

Following this call {@link #hasAltitude} will return false, + * and {@link #getAltitude} will return 0.0. + */ + public void removeAltitude() { + } + + /** + * True if this location has a speed. + */ + public boolean hasSpeed() { + return false; + } + + /** + * Get the speed if it is available, in meters/second over ground. + *

+ *

If this location does not have a speed then 0.0 is returned. + */ + public float getSpeed() { + return 0; + } + + /** + * Set the speed, in meters/second over ground. + *

+ *

Following this call {@link #hasSpeed} will return true. + */ + public void setSpeed(float speed) { + } + + /** + * Remove the speed from this location. + *

+ *

Following this call {@link #hasSpeed} will return false, + * and {@link #getSpeed} will return 0.0. + */ + public void removeSpeed() { + } + + /** + * True if this location has a bearing. + */ + public boolean hasBearing() { + return false; + } + + /** + * Get the bearing, in degrees. + *

+ *

Bearing is the horizontal direction of travel of this device, + * and is not related to the device orientation. It is guaranteed to + * be in the range (0.0, 360.0] if the device has a bearing. + *

+ *

If this location does not have a bearing then 0.0 is returned. + */ + public float getBearing() { + return 0; + } + + /** + * Set the bearing, in degrees. + *

+ *

Bearing is the horizontal direction of travel of this device, + * and is not related to the device orientation. + *

+ *

The input will be wrapped into the range (0.0, 360.0]. + */ + public void setBearing(float bearing) { + } + + /** + * Remove the bearing from this location. + *

+ *

Following this call {@link #hasBearing} will return false, + * and {@link #getBearing} will return 0.0. + */ + public void removeBearing() { + } + + /** + * True if this location has an accuracy. + *

+ *

All locations generated by the {@link LocationManager} have an + * accuracy. + */ + public boolean hasAccuracy() { + return false; + } + + /** + * Get the estimated accuracy of this location, in meters. + *

+ *

We define accuracy as the radius of 68% confidence. In other + * words, if you draw a circle centered at this location's + * latitude and longitude, and with a radius equal to the accuracy, + * then there is a 68% probability that the true location is inside + * the circle. + *

+ *

In statistical terms, it is assumed that location errors + * are random with a normal distribution, so the 68% confidence circle + * represents one standard deviation. Note that in practice, location + * errors do not always follow such a simple distribution. + *

+ *

This accuracy estimation is only concerned with horizontal + * accuracy, and does not indicate the accuracy of bearing, + * velocity or altitude if those are included in this Location. + *

+ *

If this location does not have an accuracy, then 0.0 is returned. + * All locations generated by the {@link LocationManager} include + * an accuracy. + */ + public float getAccuracy() { + return 0; + } + + /** + * Set the estimated accuracy of this location, meters. + *

+ *

See {@link #getAccuracy} for the definition of accuracy. + *

+ *

Following this call {@link #hasAccuracy} will return true. + */ + public void setAccuracy(float accuracy) { + } + + /** + * Remove the accuracy from this location. + *

+ *

Following this call {@link #hasAccuracy} will return false, and + * {@link #getAccuracy} will return 0.0. + */ + public void removeAccuracy() { + } + + /** + * Return true if this Location object is complete. + *

+ *

A location object is currently considered complete if it has + * a valid provider, accuracy, wall-clock time and elapsed real-time. + *

+ *

All locations supplied by the {@link LocationManager} to + * applications must be complete. + * + * @hide + * @see #makeComplete + */ + public boolean isComplete() { + return false; + } + + /** + * Helper to fill incomplete fields. + *

+ *

Used to assist in backwards compatibility with + * Location objects received from applications. + * + * @hide + * @see #isComplete + */ + public void makeComplete() { + } + + /** + * Returns additional provider-specific information about the + * location fix as a Bundle. The keys and values are determined + * by the provider. If no additional information is available, + * null is returned. + *

+ *

A number of common key/value pairs are listed + * below. Providers that use any of the keys on this list must + * provide the corresponding value as described below. + *

+ *

    + *
  • satellites - the number of satellites used to derive the fix + *
+ */ + public Bundle getExtras() { + return null; + } + + /** + * Sets the extra information associated with this fix to the + * given Bundle. + */ + public void setExtras(Bundle extras) { + } + + public void dump(Printer pw, String prefix) { + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Location createFromParcel(Parcel in) { + return null; + } + + @Override + public Location[] newArray(int size) { + return null; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + } + + /** + * Returns one of the optional extra {@link Location}s that can be attached + * to this Location. + * + * @param key the key associated with the desired extra Location + * @return the extra Location, or null if unavailable + * @hide + */ + public Location getExtraLocation(String key) { + return null; + } + + /** + * Attaches an extra {@link Location} to this Location. + * + * @param key the key associated with the Location extra + * @param location the Location to attach + * @hide + */ + public void setExtraLocation(String key, Location value) { + } + + /** + * Returns true if the Location came from a mock provider. + * + * @return true if this Location came from a mock provider, false otherwise + */ + public boolean isFromMockProvider() { + return false; + } + + /** + * Flag this Location as having come from a mock provider or not. + * + * @param isFromMockProvider true if this Location came from a mock provider, false otherwise + * @hide + */ + public void setIsFromMockProvider(boolean isFromMockProvider) { + } +} diff --git a/play-services-location/system-api/src/main/java/android/location/LocationManager.java b/play-services-location/system-api/src/main/java/android/location/LocationManager.java new file mode 100644 index 0000000000..cfe61d528e --- /dev/null +++ b/play-services-location/system-api/src/main/java/android/location/LocationManager.java @@ -0,0 +1,1052 @@ +/* + * SPDX-FileCopyrightText: 2007, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.location; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Looper; + +import java.util.List; + +/** + * This class provides access to the system location services. These + * services allow applications to obtain periodic updates of the + * device's geographical location, or to fire an application-specified + * {@link Intent} when the device enters the proximity of a given + * geographical location. + *

+ *

You do not + * instantiate this class directly; instead, retrieve it through + * {@link android.content.Context#getSystemService + * Context.getSystemService(Context.LOCATION_SERVICE)}. + *

+ *

Unless noted, all Location API methods require + * the {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} or + * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} permissions. + * If your application only has the coarse permission then it will not have + * access to the GPS or passive location providers. Other providers will still + * return location results, but the update rate will be throttled and the exact + * location will be obfuscated to a coarse level of accuracy. + */ +@SuppressWarnings("deprecation") +public class LocationManager { + + /** + * Name of the network location provider. + *

This provider determines location based on + * availability of cell tower and WiFi access points. Results are retrieved + * by means of a network lookup. + */ + public static final String NETWORK_PROVIDER = "network"; + + /** + * Name of the GPS location provider. + *

+ *

This provider determines location using + * satellites. Depending on conditions, this provider may take a while to return + * a location fix. Requires the permission + * {@link android.Manifest.permission#ACCESS_FINE_LOCATION}. + *

+ *

The extras Bundle for the GPS location provider can contain the + * following key/value pairs: + *

    + *
  • satellites - the number of satellites used to derive the fix + *
+ */ + public static final String GPS_PROVIDER = "gps"; + + /** + * A special location provider for receiving locations without actually initiating + * a location fix. + *

+ *

This provider can be used to passively receive location updates + * when other applications or services request them without actually requesting + * the locations yourself. This provider will return locations generated by other + * providers. You can query the {@link Location#getProvider()} method to determine + * the origin of the location update. Requires the permission + * {@link android.Manifest.permission#ACCESS_FINE_LOCATION}, although if the GPS is + * not enabled this provider might only return coarse fixes. + */ + public static final String PASSIVE_PROVIDER = "passive"; + + /** + * Name of the Fused location provider. + *

+ *

This provider combines inputs for all possible location sources + * to provide the best possible Location fix. It is implicitly + * used for all API's that involve the {@link LocationRequest} + * object. + * + * @hide + */ + public static final String FUSED_PROVIDER = "fused"; + + /** + * Key used for the Bundle extra holding a boolean indicating whether + * a proximity alert is entering (true) or exiting (false).. + */ + public static final String KEY_PROXIMITY_ENTERING = "entering"; + + /** + * Key used for a Bundle extra holding an Integer status value + * when a status change is broadcast using a PendingIntent. + */ + public static final String KEY_STATUS_CHANGED = "status"; + + /** + * Key used for a Bundle extra holding an Boolean status value + * when a provider enabled/disabled event is broadcast using a PendingIntent. + */ + public static final String KEY_PROVIDER_ENABLED = "providerEnabled"; + + /** + * Key used for a Bundle extra holding a Location value + * when a location change is broadcast using a PendingIntent. + */ + public static final String KEY_LOCATION_CHANGED = "location"; + + /** + * Broadcast intent action indicating that the GPS has either been + * enabled or disabled. An intent extra provides this state as a boolean, + * where {@code true} means enabled. + * + * @hide + * @see #EXTRA_GPS_ENABLED + */ + public static final String GPS_ENABLED_CHANGE_ACTION = + "android.location.GPS_ENABLED_CHANGE"; + + /** + * Broadcast intent action when the configured location providers + * change. For use with {@link #isProviderEnabled(String)}. If you're interacting with the + * {@link android.provider.Settings.Secure#LOCATION_MODE} API, use {@link #MODE_CHANGED_ACTION} + * instead. + */ + public static final String PROVIDERS_CHANGED_ACTION = + "android.location.PROVIDERS_CHANGED"; + + /** + * Broadcast intent action when {@link android.provider.Settings.Secure#LOCATION_MODE} changes. + * For use with the {@link android.provider.Settings.Secure#LOCATION_MODE} API. + * If you're interacting with {@link #isProviderEnabled(String)}, use + * {@link #PROVIDERS_CHANGED_ACTION} instead. + *

+ * In the future, there may be mode changes that do not result in + * {@link #PROVIDERS_CHANGED_ACTION} broadcasts. + */ + public static final String MODE_CHANGED_ACTION = "android.location.MODE_CHANGED"; + + /** + * Broadcast intent action indicating that the GPS has either started or + * stopped receiving GPS fixes. An intent extra provides this state as a + * boolean, where {@code true} means that the GPS is actively receiving fixes. + * + * @hide + * @see #EXTRA_GPS_ENABLED + */ + public static final String GPS_FIX_CHANGE_ACTION = + "android.location.GPS_FIX_CHANGE"; + + /** + * The lookup key for a boolean that indicates whether GPS is enabled or + * disabled. {@code true} means GPS is enabled. Retrieve it with + * {@link android.content.Intent#getBooleanExtra(String, boolean)}. + * + * @hide + */ + public static final String EXTRA_GPS_ENABLED = "enabled"; + + /** + * Broadcast intent action indicating that a high power location requests + * has either started or stopped being active. The current state of + * active location requests should be read from AppOpsManager using + * {@code OP_MONITOR_HIGH_POWER_LOCATION}. + * + * @hide + */ + public static final String HIGH_POWER_REQUEST_CHANGE_ACTION = + "android.location.HIGH_POWER_REQUEST_CHANGE"; + + /** + * @hide - hide this constructor because it has a parameter + * of type ILocationManager, which is a system private class. The + * right way to create an instance of this class is using the + * factory Context.getSystemService. + */ + public LocationManager(Context context, Object service) { + } + + /** + * Returns a list of the names of all known location providers. + *

All providers are returned, including ones that are not permitted to + * be accessed by the calling activity or are currently disabled. + * + * @return list of Strings containing names of the provider + */ + public List getAllProviders() { + return null; + } + + /** + * Returns a list of the names of location providers. + * + * @param enabledOnly if true then only the providers which are currently + * enabled are returned. + * @return list of Strings containing names of the providers + */ + public List getProviders(boolean enabledOnly) { + return null; + } + + /** + * Returns the information associated with the location provider of the + * given name, or null if no provider exists by that name. + * + * @param name the provider name + * @return a LocationProvider, or null + * @throws IllegalArgumentException if name is null or does not exist + * @throws SecurityException if the caller is not permitted to access the + * given provider. + */ + public LocationProvider getProvider(String name) { + return null; + } + + /** + * Returns a list of the names of LocationProviders that satisfy the given + * criteria, or null if none do. Only providers that are permitted to be + * accessed by the calling activity will be returned. + * + * @param criteria the criteria that the returned providers must match + * @param enabledOnly if true then only the providers which are currently + * enabled are returned. + * @return list of Strings containing names of the providers + */ + public List getProviders(Criteria criteria, boolean enabledOnly) { + return null; + } + + /** + * Returns the name of the provider that best meets the given criteria. Only providers + * that are permitted to be accessed by the calling activity will be + * returned. If several providers meet the criteria, the one with the best + * accuracy is returned. If no provider meets the criteria, + * the criteria are loosened in the following sequence: + *

+ *

    + *
  • power requirement + *
  • accuracy + *
  • bearing + *
  • speed + *
  • altitude + *
+ *

+ *

Note that the requirement on monetary cost is not removed + * in this process. + * + * @param criteria the criteria that need to be matched + * @param enabledOnly if true then only a provider that is currently enabled is returned + * @return name of the provider that best matches the requirements + */ + public String getBestProvider(Criteria criteria, boolean enabledOnly) { + return null; + } + + /** + * Register for location updates using the named provider, and a + * pending intent. + *

+ *

See {@link #requestLocationUpdates(long, float, Criteria, PendingIntent)} + * for more detail on how to use this method. + * + * @param provider the name of the provider with which to register + * @param minTime minimum time interval between location updates, in milliseconds + * @param minDistance minimum distance between location updates, in meters + * @param listener a {@link LocationListener} whose + * {@link LocationListener#onLocationChanged} method will be called for + * each location update + * @throws IllegalArgumentException if provider is null or doesn't exist + * on this device + * @throws IllegalArgumentException if listener is null + * @throws RuntimeException if the calling thread has no Looper + * @throws SecurityException if no suitable permission is present + */ + public void requestLocationUpdates(String provider, long minTime, float minDistance, + LocationListener listener) { + } + + /** + * Register for location updates using the named provider, and a callback on + * the specified looper thread. + *

+ *

See {@link #requestLocationUpdates(long, float, Criteria, PendingIntent)} + * for more detail on how to use this method. + * + * @param provider the name of the provider with which to register + * @param minTime minimum time interval between location updates, in milliseconds + * @param minDistance minimum distance between location updates, in meters + * @param listener a {@link LocationListener} whose + * {@link LocationListener#onLocationChanged} method will be called for + * each location update + * @param looper a Looper object whose message queue will be used to + * implement the callback mechanism, or null to make callbacks on the calling + * thread + * @throws IllegalArgumentException if provider is null or doesn't exist + * @throws IllegalArgumentException if listener is null + * @throws SecurityException if no suitable permission is present + */ + public void requestLocationUpdates(String provider, long minTime, float minDistance, + LocationListener listener, Looper looper) { + } + + /** + * Register for location updates using a Criteria, and a callback + * on the specified looper thread. + *

+ *

See {@link #requestLocationUpdates(long, float, Criteria, PendingIntent)} + * for more detail on how to use this method. + * + * @param minTime minimum time interval between location updates, in milliseconds + * @param minDistance minimum distance between location updates, in meters + * @param criteria contains parameters for the location manager to choose the + * appropriate provider and parameters to compute the location + * @param listener a {@link LocationListener} whose + * {@link LocationListener#onLocationChanged} method will be called for + * each location update + * @param looper a Looper object whose message queue will be used to + * implement the callback mechanism, or null to make callbacks on the calling + * thread + * @throws IllegalArgumentException if criteria is null + * @throws IllegalArgumentException if listener is null + * @throws SecurityException if no suitable permission is present + */ + public void requestLocationUpdates(long minTime, float minDistance, Criteria criteria, + LocationListener listener, Looper looper) { + } + + /** + * Register for location updates using the named provider, and a + * pending intent. + *

+ *

See {@link #requestLocationUpdates(long, float, Criteria, PendingIntent)} + * for more detail on how to use this method. + * + * @param provider the name of the provider with which to register + * @param minTime minimum time interval between location updates, in milliseconds + * @param minDistance minimum distance between location updates, in meters + * @param intent a {@link PendingIntent} to be sent for each location update + * @throws IllegalArgumentException if provider is null or doesn't exist + * on this device + * @throws IllegalArgumentException if intent is null + * @throws SecurityException if no suitable permission is present + */ + public void requestLocationUpdates(String provider, long minTime, float minDistance, + PendingIntent intent) { + } + + /** + * Register for location updates using a Criteria and pending intent. + *

+ *

The requestLocationUpdates() and + * requestSingleUpdate() register the current activity to be + * updated periodically by the named provider, or by the provider matching + * the specified {@link Criteria}, with location and status updates. + *

+ *

It may take a while to receive the first location update. If + * an immediate location is required, applications may use the + * {@link #getLastKnownLocation(String)} method. + *

+ *

Location updates are received either by {@link LocationListener} + * callbacks, or by broadcast intents to a supplied {@link PendingIntent}. + *

+ *

If the caller supplied a pending intent, then location updates + * are sent with a key of {@link #KEY_LOCATION_CHANGED} and a + * {@link android.location.Location} value. + *

+ *

The location update interval can be controlled using the minTime parameter. + * The elapsed time between location updates will never be less than + * minTime, although it can be more depending on the Location Provider + * implementation and the update interval requested by other applications. + *

+ *

Choosing a sensible value for minTime is important to conserve + * battery life. Each location update requires power from + * GPS, WIFI, Cell and other radios. Select a minTime value as high as + * possible while still providing a reasonable user experience. + * If your application is not in the foreground and showing + * location to the user then your application should avoid using an active + * provider (such as {@link #NETWORK_PROVIDER} or {@link #GPS_PROVIDER}), + * but if you insist then select a minTime of 5 * 60 * 1000 (5 minutes) + * or greater. If your application is in the foreground and showing + * location to the user then it is appropriate to select a faster + * update interval. + *

+ *

The minDistance parameter can also be used to control the + * frequency of location updates. If it is greater than 0 then the + * location provider will only send your application an update when + * the location has changed by at least minDistance meters, AND + * at least minTime milliseconds have passed. However it is more + * difficult for location providers to save power using the minDistance + * parameter, so minTime should be the primary tool to conserving battery + * life. + *

+ *

If your application wants to passively observe location + * updates triggered by other applications, but not consume + * any additional power otherwise, then use the {@link #PASSIVE_PROVIDER} + * This provider does not actively turn on or modify active location + * providers, so you do not need to be as careful about minTime and + * minDistance. However if your application performs heavy work + * on a location update (such as network activity) then you should + * select non-zero values for minTime and/or minDistance to rate-limit + * your update frequency in the case another application enables a + * location provider with extremely fast updates. + *

+ *

In case the provider is disabled by the user, updates will stop, + * and a provider availability update will be sent. + * As soon as the provider is enabled again, + * location updates will immediately resume and a provider availability + * update sent. Providers can also send status updates, at any time, + * with extra's specific to the provider. If a callback was supplied + * then status and availability updates are via + * {@link LocationListener#onProviderDisabled}, + * {@link LocationListener#onProviderEnabled} or + * {@link LocationListener#onStatusChanged}. Alternately, if a + * pending intent was supplied then status and availability updates + * are broadcast intents with extra keys of + * {@link #KEY_PROVIDER_ENABLED} or {@link #KEY_STATUS_CHANGED}. + *

+ *

If a {@link LocationListener} is used but with no Looper specified + * then the calling thread must already + * be a {@link android.os.Looper} thread such as the main thread of the + * calling Activity. If a Looper is specified with a {@link LocationListener} + * then callbacks are made on the supplied Looper thread. + *

+ *

Prior to Jellybean, the minTime parameter was + * only a hint, and some location provider implementations ignored it. + * From Jellybean and onwards it is mandatory for Android compatible + * devices to observe both the minTime and minDistance parameters. + * + * @param minTime minimum time interval between location updates, in milliseconds + * @param minDistance minimum distance between location updates, in meters + * @param criteria contains parameters for the location manager to choose the + * appropriate provider and parameters to compute the location + * @param intent a {@link PendingIntent} to be sent for each location update + * @throws IllegalArgumentException if criteria is null + * @throws IllegalArgumentException if intent is null + * @throws SecurityException if no suitable permission is present + */ + public void requestLocationUpdates(long minTime, float minDistance, Criteria criteria, + PendingIntent intent) { + } + + /** + * Register for a single location update using the named provider and + * a callback. + *

+ *

See {@link #requestLocationUpdates(long, float, Criteria, PendingIntent)} + * for more detail on how to use this method. + * + * @param provider the name of the provider with which to register + * @param listener a {@link LocationListener} whose + * {@link LocationListener#onLocationChanged} method will be called when + * the location update is available + * @param looper a Looper object whose message queue will be used to + * implement the callback mechanism, or null to make callbacks on the calling + * thread + * @throws IllegalArgumentException if provider is null or doesn't exist + * @throws IllegalArgumentException if listener is null + * @throws SecurityException if no suitable permission is present + */ + public void requestSingleUpdate(String provider, LocationListener listener, Looper looper) { + } + + /** + * Register for a single location update using a Criteria and + * a callback. + *

+ *

See {@link #requestLocationUpdates(long, float, Criteria, PendingIntent)} + * for more detail on how to use this method. + * + * @param criteria contains parameters for the location manager to choose the + * appropriate provider and parameters to compute the location + * @param listener a {@link LocationListener} whose + * {@link LocationListener#onLocationChanged} method will be called when + * the location update is available + * @param looper a Looper object whose message queue will be used to + * implement the callback mechanism, or null to make callbacks on the calling + * thread + * @throws IllegalArgumentException if criteria is null + * @throws IllegalArgumentException if listener is null + * @throws SecurityException if no suitable permission is present + */ + public void requestSingleUpdate(Criteria criteria, LocationListener listener, Looper looper) { + } + + /** + * Register for a single location update using a named provider and pending intent. + *

+ *

See {@link #requestLocationUpdates(long, float, Criteria, PendingIntent)} + * for more detail on how to use this method. + * + * @param provider the name of the provider with which to register + * @param intent a {@link PendingIntent} to be sent for the location update + * @throws IllegalArgumentException if provider is null or doesn't exist + * @throws IllegalArgumentException if intent is null + * @throws SecurityException if no suitable permission is present + */ + public void requestSingleUpdate(String provider, PendingIntent intent) { + } + + /** + * Register for a single location update using a Criteria and pending intent. + *

+ *

See {@link #requestLocationUpdates(long, float, Criteria, PendingIntent)} + * for more detail on how to use this method. + * + * @param criteria contains parameters for the location manager to choose the + * appropriate provider and parameters to compute the location + * @param intent a {@link PendingIntent} to be sent for the location update + * @throws IllegalArgumentException if provider is null or doesn't exist + * @throws IllegalArgumentException if intent is null + * @throws SecurityException if no suitable permission is present + */ + public void requestSingleUpdate(Criteria criteria, PendingIntent intent) { + } + + /** + * Register for fused location updates using a LocationRequest and callback. + *

+ *

Upon a location update, the system delivers the new {@link Location} to the + * provided {@link LocationListener}, by calling its {@link + * LocationListener#onLocationChanged} method.

+ *

+ *

The system will automatically select and enable the best providers + * to compute a location for your application. It may use only passive + * locations, or just a single location source, or it may fuse together + * multiple location sources in order to produce the best possible + * result, depending on the quality of service requested in the + * {@link LocationRequest}. + *

+ *

LocationRequest can be null, in which case the system will choose + * default, low power parameters for location updates. You will occasionally + * receive location updates as available, without a major power impact on the + * system. If your application just needs an occasional location update + * without any strict demands, then pass a null LocationRequest. + *

+ *

Only one LocationRequest can be registered for each unique callback + * or pending intent. So a subsequent request with the same callback or + * pending intent will over-write the previous LocationRequest. + *

+ *

If a pending intent is supplied then location updates + * are sent with a key of {@link #KEY_LOCATION_CHANGED} and a + * {@link android.location.Location} value. If a callback is supplied + * then location updates are made using the + * {@link LocationListener#onLocationChanged} callback, on the specified + * Looper thread. If a {@link LocationListener} is used + * but with a null Looper then the calling thread must already + * be a {@link android.os.Looper} thread (such as the main thread) and + * callbacks will occur on this thread. + *

+ *

Provider status updates and availability updates are deprecated + * because the system is performing provider fusion on the applications + * behalf. So {@link LocationListener#onProviderDisabled}, + * {@link LocationListener#onProviderEnabled}, {@link LocationListener#onStatusChanged} + * will not be called, and intents with extra keys of + * {@link #KEY_PROVIDER_ENABLED} or {@link #KEY_STATUS_CHANGED} will not + * be received. + *

+ *

To unregister for Location updates, use: {@link #removeUpdates(LocationListener)}. + * + * @param request quality of service required, null for default low power + * @param listener a {@link LocationListener} whose + * {@link LocationListener#onLocationChanged} method will be called when + * the location update is available + * @param looper a Looper object whose message queue will be used to + * implement the callback mechanism, or null to make callbacks on the calling + * thread + * @throws IllegalArgumentException if listener is null + * @throws SecurityException if no suitable permission is present + * @hide + */ + public void requestLocationUpdates(LocationRequest request, LocationListener listener, + Looper looper) { + } + + /** + * Register for fused location updates using a LocationRequest and a pending intent. + *

+ *

Upon a location update, the system delivers the new {@link Location} with your provided + * {@link PendingIntent}, as the value for {@link LocationManager#KEY_LOCATION_CHANGED} + * in the intent's extras.

+ *

+ *

To unregister for Location updates, use: {@link #removeUpdates(PendingIntent)}. + *

+ *

See {@link #requestLocationUpdates(LocationRequest, LocationListener, Looper)} + * for more detail. + * + * @param request quality of service required, null for default low power + * @param intent a {@link PendingIntent} to be sent for the location update + * @throws IllegalArgumentException if intent is null + * @throws SecurityException if no suitable permission is present + * @hide + */ + public void requestLocationUpdates(LocationRequest request, PendingIntent intent) { + } + + /** + * Removes all location updates for the specified LocationListener. + *

+ *

Following this call, updates will no longer + * occur for this listener. + * + * @param listener listener object that no longer needs location updates + * @throws IllegalArgumentException if listener is null + */ + public void removeUpdates(LocationListener listener) { + } + + /** + * Removes all location updates for the specified pending intent. + *

+ *

Following this call, updates will no longer for this pending intent. + * + * @param intent pending intent object that no longer needs location updates + * @throws IllegalArgumentException if intent is null + */ + public void removeUpdates(PendingIntent intent) { + } + + /** + * Set a proximity alert for the location given by the position + * (latitude, longitude) and the given radius. + *

+ *

When the device + * detects that it has entered or exited the area surrounding the + * location, the given PendingIntent will be used to create an Intent + * to be fired. + *

+ *

The fired Intent will have a boolean extra added with key + * {@link #KEY_PROXIMITY_ENTERING}. If the value is true, the device is + * entering the proximity region; if false, it is exiting. + *

+ *

Due to the approximate nature of position estimation, if the + * device passes through the given area briefly, it is possible + * that no Intent will be fired. Similarly, an Intent could be + * fired if the device passes very close to the given area but + * does not actually enter it. + *

+ *

After the number of milliseconds given by the expiration + * parameter, the location manager will delete this proximity + * alert and no longer monitor it. A value of -1 indicates that + * there should be no expiration time. + *

+ *

Internally, this method uses both {@link #NETWORK_PROVIDER} + * and {@link #GPS_PROVIDER}. + *

+ *

Before API version 17, this method could be used with + * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} or + * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}. + * From API version 17 and onwards, this method requires + * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} permission. + * + * @param latitude the latitude of the central point of the + * alert region + * @param longitude the longitude of the central point of the + * alert region + * @param radius the radius of the central point of the + * alert region, in meters + * @param expiration time for this proximity alert, in milliseconds, + * or -1 to indicate no expiration + * @param intent a PendingIntent that will be used to generate an Intent to + * fire when entry to or exit from the alert region is detected + * @throws SecurityException if {@link android.Manifest.permission#ACCESS_FINE_LOCATION} + * permission is not present + */ + public void addProximityAlert(double latitude, double longitude, float radius, long expiration, + PendingIntent intent) { + } + + /** + * Add a geofence with the specified LocationRequest quality of service. + *

+ *

When the device + * detects that it has entered or exited the area surrounding the + * location, the given PendingIntent will be used to create an Intent + * to be fired. + *

+ *

The fired Intent will have a boolean extra added with key + * {@link #KEY_PROXIMITY_ENTERING}. If the value is true, the device is + * entering the proximity region; if false, it is exiting. + *

+ *

The geofence engine fuses results from all location providers to + * provide the best balance between accuracy and power. Applications + * can choose the quality of service required using the + * {@link LocationRequest} object. If it is null then a default, + * low power geo-fencing implementation is used. It is possible to cross + * a geo-fence without notification, but the system will do its best + * to detect, using {@link LocationRequest} as a hint to trade-off + * accuracy and power. + *

+ *

The power required by the geofence engine can depend on many factors, + * such as quality and interval requested in {@link LocationRequest}, + * distance to nearest geofence and current device velocity. + * + * @param request quality of service required, null for default low power + * @param fence a geographical description of the geofence area + * @param intent pending intent to receive geofence updates + * @throws IllegalArgumentException if fence is null + * @throws IllegalArgumentException if intent is null + * @throws SecurityException if {@link android.Manifest.permission#ACCESS_FINE_LOCATION} + * permission is not present + * @hide + */ + public void addGeofence(LocationRequest request, Geofence fence, PendingIntent intent) { + } + + /** + * Removes the proximity alert with the given PendingIntent. + *

+ *

Before API version 17, this method could be used with + * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} or + * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}. + * From API version 17 and onwards, this method requires + * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} permission. + * + * @param intent the PendingIntent that no longer needs to be notified of + * proximity alerts + * @throws IllegalArgumentException if intent is null + * @throws SecurityException if {@link android.Manifest.permission#ACCESS_FINE_LOCATION} + * permission is not present + */ + public void removeProximityAlert(PendingIntent intent) { + } + + /** + * Remove a single geofence. + *

+ *

This removes only the specified geofence associated with the + * specified pending intent. All other geofences remain unchanged. + * + * @param fence a geofence previously passed to {@link #addGeofence} + * @param intent a pending intent previously passed to {@link #addGeofence} + * @throws IllegalArgumentException if fence is null + * @throws IllegalArgumentException if intent is null + * @throws SecurityException if {@link android.Manifest.permission#ACCESS_FINE_LOCATION} + * permission is not present + * @hide + */ + public void removeGeofence(Geofence fence, PendingIntent intent) { + } + + /** + * Remove all geofences registered to the specified pending intent. + * + * @param intent a pending intent previously passed to {@link #addGeofence} + * @throws IllegalArgumentException if intent is null + * @throws SecurityException if {@link android.Manifest.permission#ACCESS_FINE_LOCATION} + * permission is not present + * @hide + */ + public void removeAllGeofences(PendingIntent intent) { + } + + /** + * Returns the current enabled/disabled status of the given provider. + *

+ *

If the user has enabled this provider in the Settings menu, true + * is returned otherwise false is returned + *

+ *

Callers should instead use + * {@link android.provider.Settings.Secure#LOCATION_MODE} + * unless they depend on provider-specific APIs such as + * {@link #requestLocationUpdates(String, long, float, LocationListener)}. + *

+ *

+ * Before API version {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this + * method would throw {@link SecurityException} if the location permissions + * were not sufficient to use the specified provider. + * + * @param provider the name of the provider + * @return true if the provider exists and is enabled + * @throws IllegalArgumentException if provider is null + */ + public boolean isProviderEnabled(String provider) { + return false; + } + + /** + * Get the last known location. + *

+ *

This location could be very old so use + * {@link Location#getElapsedRealtimeNanos} to calculate its age. It can + * also return null if no previous location is available. + *

+ *

Always returns immediately. + * + * @return The last known location, or null if not available + * @throws SecurityException if no suitable permission is present + * @hide + */ + public Location getLastLocation() { + return null; + } + + /** + * Returns a Location indicating the data from the last known + * location fix obtained from the given provider. + *

+ *

This can be done + * without starting the provider. Note that this location could + * be out-of-date, for example if the device was turned off and + * moved to another location. + *

+ *

If the provider is currently disabled, null is returned. + * + * @param provider the name of the provider + * @return the last known location for the provider, or null + * @throws SecurityException if no suitable permission is present + * @throws IllegalArgumentException if provider is null or doesn't exist + */ + public Location getLastKnownLocation(String provider) { + return null; + } + + // --- Mock provider support --- + // TODO: It would be fantastic to deprecate mock providers entirely, and replace + // with something closer to LocationProviderBase.java + + /** + * Creates a mock location provider and adds it to the set of active providers. + * + * @param name the provider name + * @throws SecurityException if the ACCESS_MOCK_LOCATION permission is not present + * or the {@link android.provider.Settings.Secure#ALLOW_MOCK_LOCATION + * Settings.Secure.ALLOW_MOCK_LOCATION} system setting is not enabled + * @throws IllegalArgumentException if a provider with the given name already exists + */ + public void addTestProvider(String name, boolean requiresNetwork, boolean requiresSatellite, + boolean requiresCell, boolean hasMonetaryCost, boolean supportsAltitude, + boolean supportsSpeed, boolean supportsBearing, int powerRequirement, int accuracy) { + } + + /** + * Removes the mock location provider with the given name. + * + * @param provider the provider name + * @throws SecurityException if the ACCESS_MOCK_LOCATION permission is not present + * or the {@link android.provider.Settings.Secure#ALLOW_MOCK_LOCATION + * Settings.Secure.ALLOW_MOCK_LOCATION}} system setting is not enabled + * @throws IllegalArgumentException if no provider with the given name exists + */ + public void removeTestProvider(String provider) { + } + + /** + * Sets a mock location for the given provider. + *

This location will be used in place of any actual location from the provider. + * The location object must have a minimum number of fields set to be + * considered a valid LocationProvider Location, as per documentation + * on {@link Location} class. + * + * @param provider the provider name + * @param loc the mock location + * @throws SecurityException if the ACCESS_MOCK_LOCATION permission is not present + * or the {@link android.provider.Settings.Secure#ALLOW_MOCK_LOCATION + * Settings.Secure.ALLOW_MOCK_LOCATION}} system setting is not enabled + * @throws IllegalArgumentException if no provider with the given name exists + * @throws IllegalArgumentException if the location is incomplete + */ + public void setTestProviderLocation(String provider, Location loc) { + } + + /** + * Removes any mock location associated with the given provider. + * + * @param provider the provider name + * @throws SecurityException if the ACCESS_MOCK_LOCATION permission is not present + * or the {@link android.provider.Settings.Secure#ALLOW_MOCK_LOCATION + * Settings.Secure.ALLOW_MOCK_LOCATION}} system setting is not enabled + * @throws IllegalArgumentException if no provider with the given name exists + */ + public void clearTestProviderLocation(String provider) { + } + + /** + * Sets a mock enabled value for the given provider. This value will be used in place + * of any actual value from the provider. + * + * @param provider the provider name + * @param enabled the mock enabled value + * @throws SecurityException if the ACCESS_MOCK_LOCATION permission is not present + * or the {@link android.provider.Settings.Secure#ALLOW_MOCK_LOCATION + * Settings.Secure.ALLOW_MOCK_LOCATION}} system setting is not enabled + * @throws IllegalArgumentException if no provider with the given name exists + */ + public void setTestProviderEnabled(String provider, boolean enabled) { + } + + /** + * Removes any mock enabled value associated with the given provider. + * + * @param provider the provider name + * @throws SecurityException if the ACCESS_MOCK_LOCATION permission is not present + * or the {@link android.provider.Settings.Secure#ALLOW_MOCK_LOCATION + * Settings.Secure.ALLOW_MOCK_LOCATION}} system setting is not enabled + * @throws IllegalArgumentException if no provider with the given name exists + */ + public void clearTestProviderEnabled(String provider) { + } + + /** + * Sets mock status values for the given provider. These values will be used in place + * of any actual values from the provider. + * + * @param provider the provider name + * @param status the mock status + * @param extras a Bundle containing mock extras + * @param updateTime the mock update time + * @throws SecurityException if the ACCESS_MOCK_LOCATION permission is not present + * or the {@link android.provider.Settings.Secure#ALLOW_MOCK_LOCATION + * Settings.Secure.ALLOW_MOCK_LOCATION}} system setting is not enabled + * @throws IllegalArgumentException if no provider with the given name exists + */ + public void setTestProviderStatus(String provider, int status, Bundle extras, long updateTime) { + } + + /** + * Removes any mock status values associated with the given provider. + * + * @param provider the provider name + * @throws SecurityException if the ACCESS_MOCK_LOCATION permission is not present + * or the {@link android.provider.Settings.Secure#ALLOW_MOCK_LOCATION + * Settings.Secure.ALLOW_MOCK_LOCATION}} system setting is not enabled + * @throws IllegalArgumentException if no provider with the given name exists + */ + public void clearTestProviderStatus(String provider) { + } + + // --- GPS-specific support --- + + /** + * Adds a GPS status listener. + * + * @param listener GPS status listener object to register + * @return true if the listener was successfully added + * @throws SecurityException if the ACCESS_FINE_LOCATION permission is not present + */ + public boolean addGpsStatusListener(GpsStatus.Listener listener) { + return false; + } + + /** + * Removes a GPS status listener. + * + * @param listener GPS status listener object to remove + */ + public void removeGpsStatusListener(GpsStatus.Listener listener) { + } + + /** + * Adds an NMEA listener. + * + * @param listener a {@link GpsStatus.NmeaListener} object to register + * @return true if the listener was successfully added + * @throws SecurityException if the ACCESS_FINE_LOCATION permission is not present + */ + public boolean addNmeaListener(GpsStatus.NmeaListener listener) { + return false; + } + + /** + * Removes an NMEA listener. + * + * @param listener a {@link GpsStatus.NmeaListener} object to remove + */ + public void removeNmeaListener(GpsStatus.NmeaListener listener) { + } + +// /** +// * Adds a GPS Measurement listener. +// * +// * @param listener a {@link GpsMeasurementsEvent.Listener} object to register. +// * @return {@code true} if the listener was successfully registered, {@code false} otherwise. +// * @hide +// */ +// public boolean addGpsMeasurementListener(GpsMeasurementsEvent.Listener listener) { +// return false; +// } +// +// /** +// * Removes a GPS Measurement listener. +// * +// * @param listener a {@link GpsMeasurementsEvent.Listener} object to remove. +// * @hide +// */ +// public void removeGpsMeasurementListener(GpsMeasurementsEvent.Listener listener) { +// } +// +// /** +// * Adds a GPS Navigation Message listener. +// * +// * @param listener a {@link GpsNavigationMessageEvent.Listener} object to register. +// * @return {@code true} if the listener was successfully registered, {@code false} otherwise. +// * @hide +// */ +// public boolean addGpsNavigationMessageListener(GpsNavigationMessageEvent.Listener listener) { +// return false; +// } +// +// /** +// * Removes a GPS Navigation Message listener. +// * +// * @param listener a {@link GpsNavigationMessageEvent.Listener} object to remove. +// * @hide +// */ +// public void removeGpsNavigationMessageListener( +// GpsNavigationMessageEvent.Listener listener) { +// } + + /** + * Retrieves information about the current status of the GPS engine. + * This should only be called from the {@link GpsStatus.Listener#onGpsStatusChanged} + * callback to ensure that the data is copied atomically. + *

+ * The caller may either pass in a {@link GpsStatus} object to set with the latest + * status information, or pass null to create a new {@link GpsStatus} object. + * + * @param status object containing GPS status details, or null. + * @return status object containing updated GPS status. + */ + public GpsStatus getGpsStatus(GpsStatus status) { + return null; + } + + /** + * Sends additional commands to a location provider. + * Can be used to support provider specific extensions to the Location Manager API + * + * @param provider name of the location provider. + * @param command name of the command to send to the provider. + * @param extras optional arguments for the command (or null). + * The provider may optionally fill the extras Bundle with results from the command. + * @return true if the command succeeds. + */ + public boolean sendExtraCommand(String provider, String command, Bundle extras) { + return false; + } + + /** + * Used by NetInitiatedActivity to report user response + * for network initiated GPS fix requests. + * + * @hide + */ + public boolean sendNiResponse(int notifId, int userResponse) { + return false; + } +} diff --git a/play-services-location/system-api/src/main/java/android/location/LocationRequest.java b/play-services-location/system-api/src/main/java/android/location/LocationRequest.java new file mode 100644 index 0000000000..04530a9bc9 --- /dev/null +++ b/play-services-location/system-api/src/main/java/android/location/LocationRequest.java @@ -0,0 +1,472 @@ +/* + * SPDX-FileCopyrightText: 2012, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.location; + +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.os.WorkSource; + +/** + * A data object that contains quality of service parameters for requests + * to the {@link LocationManager}. + *

+ *

LocationRequest objects are used to request a quality of service + * for location updates from the Location Manager. + *

+ *

For example, if your application wants high accuracy location + * it should create a location request with {@link #setQuality} set to + * {@link #ACCURACY_FINE} or {@link #POWER_HIGH}, and it should set + * {@link #setInterval} to less than one second. This would be + * appropriate for mapping applications that are showing your location + * in real-time. + *

+ *

At the other extreme, if you want negligible power + * impact, but to still receive location updates when available, then use + * {@link #setQuality} with {@link #POWER_NONE}. With this request your + * application will not trigger (and therefore will not receive any + * power blame) any location updates, but will receive locations + * triggered by other applications. This would be appropriate for + * applications that have no firm requirement for location, but can + * take advantage when available. + *

+ *

In between these two extremes is a very common use-case, where + * applications definitely want to receive + * updates at a specified interval, and can receive them faster when + * available, but still want a low power impact. These applications + * should consider {@link #POWER_LOW} combined with a faster + * {@link #setFastestInterval} (such as 1 minute) and a slower + * {@link #setInterval} (such as 60 minutes). They will only be assigned + * power blame for the interval set by {@link #setInterval}, but can + * still receive locations triggered by other applications at a rate up + * to {@link #setFastestInterval}. This style of request is appropriate for + * many location aware applications, including background usage. Do be + * careful to also throttle {@link #setFastestInterval} if you perform + * heavy-weight work after receiving an update - such as using the network. + *

+ *

Activities should strongly consider removing all location + * request when entering the background + * (for example at {@link android.app.Activity#onPause}), or + * at least swap the request to a larger interval and lower quality. + * Future version of the location manager may automatically perform background + * throttling on behalf of applications. + *

+ *

Applications cannot specify the exact location sources that are + * used by Android's Fusion Engine. In fact, the system + * may have multiple location sources (providers) running and may + * fuse the results from several sources into a single Location object. + *

+ *

Location requests from applications with + * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} and not + * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} will + * be automatically throttled to a slower interval, and the location + * object will be obfuscated to only show a coarse level of accuracy. + *

+ *

All location requests are considered hints, and you may receive + * locations that are more accurate, less accurate, and slower + * than requested. + * + * @hide + */ +public final class LocationRequest implements Parcelable { + /** + * Used with {@link #setQuality} to request the most accurate locations available. + *

+ *

This may be up to 1 meter accuracy, although this is implementation dependent. + */ + public static final int ACCURACY_FINE = 100; + + /** + * Used with {@link #setQuality} to request "block" level accuracy. + *

+ *

Block level accuracy is considered to be about 100 meter accuracy, + * although this is implementation dependent. Using a coarse accuracy + * such as this often consumes less power. + */ + public static final int ACCURACY_BLOCK = 102; + + /** + * Used with {@link #setQuality} to request "city" level accuracy. + *

+ *

City level accuracy is considered to be about 10km accuracy, + * although this is implementation dependent. Using a coarse accuracy + * such as this often consumes less power. + */ + public static final int ACCURACY_CITY = 104; + + /** + * Used with {@link #setQuality} to require no direct power impact (passive locations). + *

+ *

This location request will not trigger any active location requests, + * but will receive locations triggered by other applications. Your application + * will not receive any direct power blame for location work. + */ + public static final int POWER_NONE = 200; + + /** + * Used with {@link #setQuality} to request low power impact. + *

+ *

This location request will avoid high power location work where + * possible. + */ + public static final int POWER_LOW = 201; + + /** + * Used with {@link #setQuality} to allow high power consumption for location. + *

+ *

This location request will allow high power location work. + */ + public static final int POWER_HIGH = 203; + + /** + * Create a location request with default parameters. + *

+ *

Default parameters are for a low power, slowly updated location. + * It can then be adjusted as required by the applications before passing + * to the {@link LocationManager} + * + * @return a new location request + */ + public static LocationRequest create() { + return null; + } + + /** + * @hide + */ + public static LocationRequest createFromDeprecatedProvider(String provider, long minTime, + float minDistance, boolean singleShot) { + return null; + } + + /** + * @hide + */ + public static LocationRequest createFromDeprecatedCriteria(Criteria criteria, long minTime, + float minDistance, boolean singleShot) { + return null; + } + + /** + * @hide + */ + public LocationRequest() { + } + + /** + * @hide + */ + public LocationRequest(LocationRequest src) { + } + + /** + * Set the quality of the request. + *

+ *

Use with a accuracy constant such as {@link #ACCURACY_FINE}, or a power + * constant such as {@link #POWER_LOW}. You cannot request both and accuracy and + * power, only one or the other can be specified. The system will then + * maximize accuracy or minimize power as appropriate. + *

+ *

The quality of the request is a strong hint to the system for which + * location sources to use. For example, {@link #ACCURACY_FINE} is more likely + * to use GPS, and {@link #POWER_LOW} is more likely to use WIFI & Cell tower + * positioning, but it also depends on many other factors (such as which sources + * are available) and is implementation dependent. + *

+ *

{@link #setQuality} and {@link #setInterval} are the most important parameters + * on a location request. + * + * @param quality an accuracy or power constant + * @return the same object, so that setters can be chained + * @throws InvalidArgumentException if the quality constant is not valid + */ + public LocationRequest setQuality(int quality) { + return null; + } + + /** + * Get the quality of the request. + * + * @return an accuracy or power constant + */ + public int getQuality() { + return 0; + } + + /** + * Set the desired interval for active location updates, in milliseconds. + *

+ *

The location manager will actively try to obtain location updates + * for your application at this interval, so it has a + * direct influence on the amount of power used by your application. + * Choose your interval wisely. + *

+ *

This interval is inexact. You may not receive updates at all (if + * no location sources are available), or you may receive them + * slower than requested. You may also receive them faster than + * requested (if other applications are requesting location at a + * faster interval). The fastest rate that that you will receive + * updates can be controlled with {@link #setFastestInterval}. + *

+ *

Applications with only the coarse location permission may have their + * interval silently throttled. + *

+ *

An interval of 0 is allowed, but not recommended, since + * location updates may be extremely fast on future implementations. + *

+ *

{@link #setQuality} and {@link #setInterval} are the most important parameters + * on a location request. + * + * @param millis desired interval in millisecond, inexact + * @return the same object, so that setters can be chained + * @throws InvalidArgumentException if the interval is less than zero + */ + public LocationRequest setInterval(long millis) { + return null; + } + + /** + * Get the desired interval of this request, in milliseconds. + * + * @return desired interval in milliseconds, inexact + */ + public long getInterval() { + return 0; + } + + /** + * Explicitly set the fastest interval for location updates, in + * milliseconds. + *

+ *

This controls the fastest rate at which your application will + * receive location updates, which might be faster than + * {@link #setInterval} in some situations (for example, if other + * applications are triggering location updates). + *

+ *

This allows your application to passively acquire locations + * at a rate faster than it actively acquires locations, saving power. + *

+ *

Unlike {@link #setInterval}, this parameter is exact. Your + * application will never receive updates faster than this value. + *

+ *

If you don't call this method, a fastest interval + * will be selected for you. It will be a value faster than your + * active interval ({@link #setInterval}). + *

+ *

An interval of 0 is allowed, but not recommended, since + * location updates may be extremely fast on future implementations. + *

+ *

If {@link #setFastestInterval} is set slower than {@link #setInterval}, + * then your effective fastest interval is {@link #setInterval}. + * + * @param millis fastest interval for updates in milliseconds, exact + * @return the same object, so that setters can be chained + * @throws InvalidArgumentException if the interval is less than zero + */ + public LocationRequest setFastestInterval(long millis) { + return null; + } + + /** + * Get the fastest interval of this request, in milliseconds. + *

+ *

The system will never provide location updates faster + * than the minimum of {@link #getFastestInterval} and + * {@link #getInterval}. + * + * @return fastest interval in milliseconds, exact + */ + public long getFastestInterval() { + return 0; + } + + /** + * Set the duration of this request, in milliseconds. + *

+ *

The duration begins immediately (and not when the request + * is passed to the location manager), so call this method again + * if the request is re-used at a later time. + *

+ *

The location manager will automatically stop updates after + * the request expires. + *

+ *

The duration includes suspend time. Values less than 0 + * are allowed, but indicate that the request has already expired. + * + * @param millis duration of request in milliseconds + * @return the same object, so that setters can be chained + */ + public LocationRequest setExpireIn(long millis) { + return null; + } + + /** + * Set the request expiration time, in millisecond since boot. + *

+ *

This expiration time uses the same time base as {@link SystemClock#elapsedRealtime}. + *

+ *

The location manager will automatically stop updates after + * the request expires. + *

+ *

The duration includes suspend time. Values before {@link SystemClock#elapsedRealtime} + * are allowed, but indicate that the request has already expired. + * + * @param millis expiration time of request, in milliseconds since boot including suspend + * @return the same object, so that setters can be chained + */ + public LocationRequest setExpireAt(long millis) { + return null; + } + + /** + * Get the request expiration time, in milliseconds since boot. + *

+ *

This value can be compared to {@link SystemClock#elapsedRealtime} to determine + * the time until expiration. + * + * @return expiration time of request, in milliseconds since boot including suspend + */ + public long getExpireAt() { + return 0; + } + + /** + * Set the number of location updates. + *

+ *

By default locations are continuously updated until the request is explicitly + * removed, however you can optionally request a set number of updates. + * For example, if your application only needs a single fresh location, + * then call this method with a value of 1 before passing the request + * to the location manager. + * + * @param numUpdates the number of location updates requested + * @return the same object, so that setters can be chained + * @throws InvalidArgumentException if numUpdates is 0 or less + */ + public LocationRequest setNumUpdates(int numUpdates) { + return null; + } + + /** + * Get the number of updates requested. + *

+ *

By default this is {@link Integer#MAX_VALUE}, which indicates that + * locations are updated until the request is explicitly removed. + * + * @return number of updates + */ + public int getNumUpdates() { + return 0; + } + + /** + * @hide + */ + public void decrementNumUpdates() { + } + + /** + * @hide + */ + public LocationRequest setProvider(String provider) { + return null; + } + + /** + * @hide + */ + public String getProvider() { + return null; + } + + /** + * @hide + */ + public LocationRequest setSmallestDisplacement(float meters) { + return null; + } + + /** + * @hide + */ + public float getSmallestDisplacement() { + return 0; + } + + /** + * Sets the WorkSource to use for power blaming of this location request. + *

+ *

No permissions are required to make this call, however the LocationManager + * will throw a SecurityException when requesting location updates if the caller + * doesn't have the {@link android.Manifest.permission#UPDATE_DEVICE_STATS} permission. + * + * @param workSource WorkSource defining power blame for this location request. + * @hide + */ + public void setWorkSource(WorkSource workSource) { + } + + /** + * @hide + */ + public WorkSource getWorkSource() { + return null; + } + + /** + * Sets whether or not this location request should be hidden from AppOps. + *

+ *

Hiding a location request from AppOps will remove user visibility in the UI as to this + * request's existence. It does not affect power blaming in the Battery page. + *

+ *

No permissions are required to make this call, however the LocationManager + * will throw a SecurityException when requesting location updates if the caller + * doesn't have the {@link android.Manifest.permission#UPDATE_APP_OPS_STATS} permission. + * + * @param hideFromAppOps If true AppOps won't keep track of this location request. + * @hide + * @see android.app.AppOpsManager + */ + public void setHideFromAppOps(boolean hideFromAppOps) { + } + + /** + * @hide + */ + public boolean getHideFromAppOps() { + return false; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public LocationRequest createFromParcel(Parcel in) { + return null; + } + + @Override + public LocationRequest[] newArray(int size) { + return null; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + } + + /** + * @hide + */ + public static String qualityToString(int quality) { + return null; + } +} diff --git a/play-services-location/system-api/src/main/java/android/net/wifi/WifiScanner.java b/play-services-location/system-api/src/main/java/android/net/wifi/WifiScanner.java new file mode 100644 index 0000000000..3e8f472310 --- /dev/null +++ b/play-services-location/system-api/src/main/java/android/net/wifi/WifiScanner.java @@ -0,0 +1,1812 @@ +/* + * SPDX-FileCopyrightText: 2008, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.net.wifi; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; +import android.os.WorkSource; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * This class provides a way to scan the Wifi universe around the device + * + * @hide + */ +public class WifiScanner { + + /** + * @hide + */ + public static final int WIFI_BAND_INDEX_24_GHZ = 0; + /** + * @hide + */ + public static final int WIFI_BAND_INDEX_5_GHZ = 1; + /** + * @hide + */ + public static final int WIFI_BAND_INDEX_5_GHZ_DFS_ONLY = 2; + /** + * @hide + */ + public static final int WIFI_BAND_INDEX_6_GHZ = 3; + /** + * @hide + */ + public static final int WIFI_BAND_INDEX_60_GHZ = 4; + /** + * @hide + */ + public static final int WIFI_BAND_COUNT = 5; + + /** + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + WIFI_BAND_INDEX_24_GHZ, + WIFI_BAND_INDEX_5_GHZ, + WIFI_BAND_INDEX_5_GHZ_DFS_ONLY, + WIFI_BAND_INDEX_6_GHZ, + WIFI_BAND_INDEX_60_GHZ}) + public @interface WifiBandIndex { + } + + /** + * no band specified; use channel list instead + */ + public static final int WIFI_BAND_UNSPECIFIED = 0; + /** + * 2.4 GHz band + */ + public static final int WIFI_BAND_24_GHZ = 1 << WIFI_BAND_INDEX_24_GHZ; + /** + * 5 GHz band excluding DFS channels + */ + public static final int WIFI_BAND_5_GHZ = 1 << WIFI_BAND_INDEX_5_GHZ; + /** + * DFS channels from 5 GHz band only + */ + public static final int WIFI_BAND_5_GHZ_DFS_ONLY = 1 << WIFI_BAND_INDEX_5_GHZ_DFS_ONLY; + /** + * 6 GHz band + */ + public static final int WIFI_BAND_6_GHZ = 1 << WIFI_BAND_INDEX_6_GHZ; + /** + * 60 GHz band + */ + public static final int WIFI_BAND_60_GHZ = 1 << WIFI_BAND_INDEX_60_GHZ; + + /** + * Combination of bands + * Note that those are only the common band combinations, + * other combinations can be created by combining any of the basic bands above + */ + /** + * Both 2.4 GHz band and 5 GHz band; no DFS channels + */ + public static final int WIFI_BAND_BOTH = WIFI_BAND_24_GHZ | WIFI_BAND_5_GHZ; + /** + * 2.4Ghz band + DFS channels from 5 GHz band only + * + * @hide + */ + public static final int WIFI_BAND_24_GHZ_WITH_5GHZ_DFS = + WIFI_BAND_24_GHZ | WIFI_BAND_5_GHZ_DFS_ONLY; + /** + * 5 GHz band including DFS channels + */ + public static final int WIFI_BAND_5_GHZ_WITH_DFS = WIFI_BAND_5_GHZ | WIFI_BAND_5_GHZ_DFS_ONLY; + /** + * Both 2.4 GHz band and 5 GHz band; with DFS channels + */ + public static final int WIFI_BAND_BOTH_WITH_DFS = + WIFI_BAND_24_GHZ | WIFI_BAND_5_GHZ | WIFI_BAND_5_GHZ_DFS_ONLY; + /** + * 2.4 GHz band and 5 GHz band (no DFS channels) and 6 GHz + */ + public static final int WIFI_BAND_24_5_6_GHZ = WIFI_BAND_BOTH | WIFI_BAND_6_GHZ; + /** + * 2.4 GHz band and 5 GHz band; with DFS channels and 6 GHz + */ + public static final int WIFI_BAND_24_5_WITH_DFS_6_GHZ = + WIFI_BAND_BOTH_WITH_DFS | WIFI_BAND_6_GHZ; + /** + * @hide + */ + public static final int WIFI_BAND_24_5_6_60_GHZ = + WIFI_BAND_24_5_6_GHZ | WIFI_BAND_60_GHZ; + /** + * @hide + */ + public static final int WIFI_BAND_24_5_WITH_DFS_6_60_GHZ = + WIFI_BAND_24_5_6_60_GHZ | WIFI_BAND_5_GHZ_DFS_ONLY; + + /** + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + WIFI_BAND_UNSPECIFIED, + WIFI_BAND_24_GHZ, + WIFI_BAND_5_GHZ, + WIFI_BAND_BOTH, + WIFI_BAND_5_GHZ_DFS_ONLY, + WIFI_BAND_24_GHZ_WITH_5GHZ_DFS, + WIFI_BAND_5_GHZ_WITH_DFS, + WIFI_BAND_BOTH_WITH_DFS, + WIFI_BAND_6_GHZ, + WIFI_BAND_24_5_6_GHZ, + WIFI_BAND_24_5_WITH_DFS_6_GHZ, + WIFI_BAND_60_GHZ, + WIFI_BAND_24_5_6_60_GHZ, + WIFI_BAND_24_5_WITH_DFS_6_60_GHZ}) + public @interface WifiBand { + } + + /** + * All bands + * + * @hide + */ + public static final int WIFI_BAND_ALL = (1 << WIFI_BAND_COUNT) - 1; + + /** + * Minimum supported scanning period + */ + public static final int MIN_SCAN_PERIOD_MS = 1000; + /** + * Maximum supported scanning period + */ + public static final int MAX_SCAN_PERIOD_MS = 1024000; + + /** + * No Error + */ + public static final int REASON_SUCCEEDED = 0; + /** + * Unknown error + */ + public static final int REASON_UNSPECIFIED = -1; + /** + * Invalid listener + */ + public static final int REASON_INVALID_LISTENER = -2; + /** + * Invalid request + */ + public static final int REASON_INVALID_REQUEST = -3; + /** + * Invalid request + */ + public static final int REASON_NOT_AUTHORIZED = -4; + /** + * An outstanding request with the same listener hasn't finished yet. + */ + public static final int REASON_DUPLICATE_REQEUST = -5; + + /** + * @hide + */ + public static final String GET_AVAILABLE_CHANNELS_EXTRA = "Channels"; + + /** + * This constant is used for {@link ScanSettings#setRnrSetting(int)}. + *

+ * Scan 6Ghz APs co-located with 2.4/5Ghz APs using Reduced Neighbor Report (RNR) if the 6Ghz + * band is explicitly requested to be scanned and the current country code supports scanning + * of at least one 6Ghz channel. The 6Ghz band is explicitly requested if the + * ScanSetting.band parameter is set to one of: + *

  • {@link #WIFI_BAND_6_GHZ}
  • + *
  • {@link #WIFI_BAND_24_5_6_GHZ}
  • + *
  • {@link #WIFI_BAND_24_5_WITH_DFS_6_GHZ}
  • + *
  • {@link #WIFI_BAND_24_5_6_60_GHZ}
  • + *
  • {@link #WIFI_BAND_24_5_WITH_DFS_6_60_GHZ}
  • + *
  • {@link #WIFI_BAND_ALL}
  • + **/ + public static final int WIFI_RNR_ENABLED_IF_WIFI_BAND_6_GHZ_SCANNED = 0; + /** + * This constant is used for {@link ScanSettings#setRnrSetting(int)}. + *

    + * Request to scan 6Ghz APs co-located with 2.4/5Ghz APs using Reduced Neighbor Report (RNR) + * when the current country code supports scanning of at least one 6Ghz channel. + **/ + public static final int WIFI_RNR_ENABLED = 1; + /** + * This constant is used for {@link ScanSettings#setRnrSetting(int)}. + *

    + * Do not request to scan 6Ghz APs co-located with 2.4/5Ghz APs using + * Reduced Neighbor Report (RNR) + **/ + public static final int WIFI_RNR_NOT_NEEDED = 2; + + /** + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + WIFI_RNR_ENABLED_IF_WIFI_BAND_6_GHZ_SCANNED, + WIFI_RNR_ENABLED, + WIFI_RNR_NOT_NEEDED}) + public @interface RnrSetting { + } + + /** + * Generic action callback invocation interface + * + * @hide + */ + public static interface ActionListener { + public void onSuccess(); + + public void onFailure(int reason, String description); + } + + /** + * Test if scan is a full scan. i.e. scanning all available bands. + * For backward compatibility, since some apps don't include 6GHz or 60Ghz in their requests + * yet, lacking 6GHz or 60Ghz band does not cause the result to be false. + * + * @param bandsScanned bands that are fully scanned + * @param excludeDfs when true, DFS band is excluded from the check + * @return true if all bands are scanned, false otherwise + * @hide + */ + public static boolean isFullBandScan(@WifiBand int bandsScanned, boolean excludeDfs) { + return (bandsScanned | WIFI_BAND_6_GHZ | WIFI_BAND_60_GHZ + | (excludeDfs ? WIFI_BAND_5_GHZ_DFS_ONLY : 0)) + == WIFI_BAND_ALL; + } + + /** + * Returns a list of all the possible channels for the given band(s). + * + * @param band one of the WifiScanner#WIFI_BAND_* constants, e.g. {@link #WIFI_BAND_24_GHZ} + * @return a list of all the frequencies, in MHz, for the given band(s) e.g. channel 1 is + * 2412, or null if an error occurred. + * @hide + */ + @NonNull + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public List getAvailableChannels(int band) { + throw new UnsupportedOperationException(); + } + + /** + * provides channel specification for scanning + */ + public static class ChannelSpec { + /** + * channel frequency in MHz; for example channel 1 is specified as 2412 + */ + public int frequency; + /** + * if true, scan this channel in passive fashion. + * This flag is ignored on DFS channel specification. + * + * @hide + */ + public boolean passive; /* ignored on DFS channels */ + /** + * how long to dwell on this channel + * + * @hide + */ + public int dwellTimeMS; /* not supported for now */ + + /** + * default constructor for channel spec + */ + public ChannelSpec(int frequency) { + this.frequency = frequency; + passive = false; + dwellTimeMS = 0; + } + } + + /** + * reports {@link ScanListener#onResults} when underlying buffers are full + * this is simply the lack of the {@link #REPORT_EVENT_AFTER_EACH_SCAN} flag + * + * @deprecated It is not supported anymore. + */ + @Deprecated + public static final int REPORT_EVENT_AFTER_BUFFER_FULL = 0; + /** + * reports {@link ScanListener#onResults} after each scan + */ + public static final int REPORT_EVENT_AFTER_EACH_SCAN = (1 << 0); + /** + * reports {@link ScanListener#onFullResult} whenever each beacon is discovered + */ + public static final int REPORT_EVENT_FULL_SCAN_RESULT = (1 << 1); + /** + * Do not place scans in the chip's scan history buffer + */ + public static final int REPORT_EVENT_NO_BATCH = (1 << 2); + + /** + * Optimize the scan for lower latency. + * + * @see ScanSettings#type + */ + public static final int SCAN_TYPE_LOW_LATENCY = 0; + /** + * Optimize the scan for lower power usage. + * + * @see ScanSettings#type + */ + public static final int SCAN_TYPE_LOW_POWER = 1; + /** + * Optimize the scan for higher accuracy. + * + * @see ScanSettings#type + */ + public static final int SCAN_TYPE_HIGH_ACCURACY = 2; + /** + * Max valid value of SCAN_TYPE_ + * + * @hide + */ + public static final int SCAN_TYPE_MAX = 2; + + /** + * {@hide} + */ + public static final String SCAN_PARAMS_SCAN_SETTINGS_KEY = "ScanSettings"; + /** + * {@hide} + */ + public static final String SCAN_PARAMS_WORK_SOURCE_KEY = "WorkSource"; + /** + * {@hide} + */ + public static final String REQUEST_PACKAGE_NAME_KEY = "PackageName"; + /** + * {@hide} + */ + public static final String REQUEST_FEATURE_ID_KEY = "FeatureId"; + + /** + * scan configuration parameters to be sent to {@link #startBackgroundScan} + */ + public static class ScanSettings implements Parcelable { + /** + * Hidden network to be scanned for. + */ + public static class HiddenNetwork { + /** + * SSID of the network + */ + @NonNull + public final String ssid; + + /** + * Default constructor for HiddenNetwork. + */ + public HiddenNetwork(@NonNull String ssid) { + this.ssid = ssid; + } + } + + /** + * one of the WIFI_BAND values + */ + public int band; + /** + * one of the {@code WIFI_RNR_*} values. + */ + private int mRnrSetting = WIFI_RNR_ENABLED_IF_WIFI_BAND_6_GHZ_SCANNED; + + /** + * See {@link #set6GhzPscOnlyEnabled} + */ + private boolean mEnable6GhzPsc = false; + + /** + * list of channels; used when band is set to WIFI_BAND_UNSPECIFIED + */ + public ChannelSpec[] channels; + /** + * List of hidden networks to scan for. Explicit probe requests are sent out for such + * networks during scan. Only valid for single scan requests. + */ + @NonNull + public final List hiddenNetworks = new ArrayList<>(); + /** + * period of background scan; in millisecond, 0 => single shot scan + * + * @deprecated Background scan support has always been hardware vendor dependent. This + * support may not be present on newer devices. Use {@link #startScan(ScanSettings, + * ScanListener)} instead for single scans. + */ + @Deprecated + public int periodInMs; + /** + * must have a valid REPORT_EVENT value + * + * @deprecated Background scan support has always been hardware vendor dependent. This + * support may not be present on newer devices. Use {@link #startScan(ScanSettings, + * ScanListener)} instead for single scans. + */ + @Deprecated + public int reportEvents; + /** + * defines number of bssids to cache from each scan + * + * @deprecated Background scan support has always been hardware vendor dependent. This + * support may not be present on newer devices. Use {@link #startScan(ScanSettings, + * ScanListener)} instead for single scans. + */ + @Deprecated + public int numBssidsPerScan; + /** + * defines number of scans to cache; use it with REPORT_EVENT_AFTER_BUFFER_FULL + * to wake up at fixed interval + * + * @deprecated Background scan support has always been hardware vendor dependent. This + * support may not be present on newer devices. Use {@link #startScan(ScanSettings, + * ScanListener)} instead for single scans. + */ + @Deprecated + public int maxScansToCache; + /** + * if maxPeriodInMs is non zero or different than period, then this bucket is + * a truncated binary exponential backoff bucket and the scan period will grow + * exponentially as per formula: actual_period(N) = period * (2 ^ (N/stepCount)) + * to maxPeriodInMs + * + * @deprecated Background scan support has always been hardware vendor dependent. This + * support may not be present on newer devices. Use {@link #startScan(ScanSettings, + * ScanListener)} instead for single scans. + */ + @Deprecated + public int maxPeriodInMs; + /** + * for truncated binary exponential back off bucket, number of scans to perform + * for a given period + * + * @deprecated Background scan support has always been hardware vendor dependent. This + * support may not be present on newer devices. Use {@link #startScan(ScanSettings, + * ScanListener)} instead for single scans. + */ + @Deprecated + public int stepCount; + /** + * Flag to indicate if the scan settings are targeted for PNO scan. + * {@hide} + */ + public boolean isPnoScan; + public int type = SCAN_TYPE_LOW_LATENCY; + /** + * This scan request may ignore location settings while receiving scans. This should only + * be used in emergency situations. + * {@hide} + */ + public boolean ignoreLocationSettings; + /** + * This scan request will be hidden from app-ops noting for location information. This + * should only be used by FLP/NLP module on the device which is using the scan results to + * compute results for behalf on their clients. FLP/NLP module using this flag should ensure + * that they note in app-ops the eventual delivery of location information computed using + * these results to their client . + * {@hide} + */ + public boolean hideFromAppOps; + + /** + * Configure whether it is needed to scan 6Ghz non Preferred Scanning Channels when scanning + * {@link #WIFI_BAND_6_GHZ}. If set to true and a band that contains + * {@link #WIFI_BAND_6_GHZ} is configured for scanning, then only scan 6Ghz PSC channels in + * addition to any other bands configured for scanning. Note, 6Ghz non-PSC channels that + * are co-located with 2.4/5Ghz APs could still be scanned via the + * {@link #setRnrSetting(int)} API. + * + *

    + * For example, given a ScanSettings with band set to {@link #WIFI_BAND_24_5_WITH_DFS_6_GHZ} + * If this API is set to "true" then the ScanSettings is configured to scan all of 2.4Ghz + * + all of 5Ghz(DFS and non-DFS) + 6Ghz PSC channels. If this API is set to "false", then + * the ScanSetting is configured to scan all of 2.4Ghz + all of 5Ghz(DFS and non_DFS) + * + all of 6Ghz channels. + * + * @param enable true to only scan 6Ghz PSC channels, false to scan all 6Ghz channels. + */ + @RequiresApi(Build.VERSION_CODES.S) + public void set6GhzPscOnlyEnabled(boolean enable) { + throw new UnsupportedOperationException(); + } + + /** + * See {@link #set6GhzPscOnlyEnabled} + */ + @RequiresApi(Build.VERSION_CODES.S) + public boolean is6GhzPscOnlyEnabled() { + throw new UnsupportedOperationException(); + } + + /** + * Configure when to scan 6Ghz APs co-located with 2.4/5Ghz APs using Reduced + * Neighbor Report (RNR). + * + * @param rnrSetting one of the {@code WIFI_RNR_*} values + */ + @RequiresApi(Build.VERSION_CODES.S) + public void setRnrSetting(@RnrSetting int rnrSetting) { + throw new UnsupportedOperationException(); + } + + /** + * See {@link #setRnrSetting} + */ + @RequiresApi(Build.VERSION_CODES.S) + public @RnrSetting int getRnrSetting() { + throw new UnsupportedOperationException(); + } + + /** + * Implement the Parcelable interface {@hide} + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public void writeToParcel(Parcel dest, int flags) { + throw new UnsupportedOperationException(); + } + + /** + * Implement the Parcelable interface {@hide} + */ + public static final @NonNull Creator CREATOR = + new Creator() { + public ScanSettings createFromParcel(Parcel in) { + throw new UnsupportedOperationException(); + } + + public ScanSettings[] newArray(int size) { + return new ScanSettings[size]; + } + }; + } + + /** + * All the information garnered from a single scan + */ + public static class ScanData implements Parcelable { + /** + * scan identifier + */ + private int mId; + /** + * additional information about scan + * 0 => no special issues encountered in the scan + * non-zero => scan was truncated, so results may not be complete + */ + private int mFlags; + /** + * Indicates the buckets that were scanned to generate these results. + * This is not relevant to WifiScanner API users and is used internally. + * {@hide} + */ + private int mBucketsScanned; + /** + * Bands scanned. One of the WIFI_BAND values. + * Will be {@link #WIFI_BAND_UNSPECIFIED} if the list of channels do not fully cover + * any of the bands. + * {@hide} + */ + private int mScannedBands; + /** + * all scan results discovered in this scan, sorted by timestamp in ascending order + */ + private final List mResults; + + ScanData() { + mResults = new ArrayList<>(); + } + + public ScanData(int id, int flags, ScanResult[] results) { + mId = id; + mFlags = flags; + mResults = new ArrayList<>(Arrays.asList(results)); + } + + /** + * {@hide} + */ + public ScanData(int id, int flags, int bucketsScanned, int bandsScanned, + ScanResult[] results) { + this(id, flags, bucketsScanned, bandsScanned, new ArrayList<>(Arrays.asList(results))); + } + + /** + * {@hide} + */ + public ScanData(int id, int flags, int bucketsScanned, int bandsScanned, + List results) { + mId = id; + mFlags = flags; + mBucketsScanned = bucketsScanned; + mScannedBands = bandsScanned; + mResults = results; + } + + public ScanData(ScanData s) { + throw new UnsupportedOperationException(); + } + + public int getId() { + return mId; + } + + public int getFlags() { + return mFlags; + } + + /** + * {@hide} + */ + public int getBucketsScanned() { + return mBucketsScanned; + } + + /** + * Retrieve the bands that were fully scanned for this ScanData instance. "fully" here + * refers to all the channels available in the band based on the current regulatory + * domain. + * + * @return Bitmask of {@link #WIFI_BAND_24_GHZ}, {@link #WIFI_BAND_5_GHZ}, + * {@link #WIFI_BAND_5_GHZ_DFS_ONLY}, {@link #WIFI_BAND_6_GHZ} & {@link #WIFI_BAND_60_GHZ} + * values. Each bit is set only if all the channels in the corresponding band is scanned. + * Will be {@link #WIFI_BAND_UNSPECIFIED} if the list of channels do not fully cover + * any of the bands. + *

    + * For ex: + *

  • Scenario 1: Fully scanned 2.4Ghz band, partially scanned 5Ghz band + * - Returns {@link #WIFI_BAND_24_GHZ} + *
  • + *
  • Scenario 2: Partially scanned 2.4Ghz band and 5Ghz band + * - Returns {@link #WIFI_BAND_UNSPECIFIED} + *
  • + *

    + */ + public @WifiBand int getScannedBands() { + return getScannedBandsInternal(); + } + + /** + * Same as {@link #getScannedBands()}. For use in the wifi stack without version check. + *

    + * {@hide} + */ + public @WifiBand int getScannedBandsInternal() { + return mScannedBands; + } + + public ScanResult[] getResults() { + return mResults.toArray(new ScanResult[0]); + } + + /** + * {@hide} + */ + public void addResults(@NonNull ScanResult[] newResults) { + throw new UnsupportedOperationException(); + } + + /** + * {@hide} + */ + public void addResults(@NonNull ScanData s) { + mScannedBands |= s.mScannedBands; + mFlags |= s.mFlags; + addResults(s.getResults()); + } + + /** + * {@hide} + */ + public boolean isFullBandScanResults() { + return (mScannedBands & WifiScanner.WIFI_BAND_24_GHZ) != 0 + && (mScannedBands & WifiScanner.WIFI_BAND_5_GHZ) != 0; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public void writeToParcel(Parcel dest, int flags) { + throw new UnsupportedOperationException(); + } + + /** + * Implement the Parcelable interface {@hide} + */ + public static final @NonNull Creator CREATOR = + new Creator() { + public ScanData createFromParcel(Parcel in) { + throw new UnsupportedOperationException(); + } + + public ScanData[] newArray(int size) { + return new ScanData[size]; + } + }; + } + + public static class ParcelableScanData implements Parcelable { + + public ScanData mResults[]; + + public ParcelableScanData(ScanData[] results) { + mResults = results; + } + + public ScanData[] getResults() { + return mResults; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public void writeToParcel(Parcel dest, int flags) { + if (mResults != null) { + dest.writeInt(mResults.length); + for (int i = 0; i < mResults.length; i++) { + ScanData result = mResults[i]; + result.writeToParcel(dest, flags); + } + } else { + dest.writeInt(0); + } + } + + /** + * Implement the Parcelable interface {@hide} + */ + public static final @NonNull Creator CREATOR = + new Creator() { + public ParcelableScanData createFromParcel(Parcel in) { + int n = in.readInt(); + ScanData results[] = new ScanData[n]; + for (int i = 0; i < n; i++) { + results[i] = ScanData.CREATOR.createFromParcel(in); + } + return new ParcelableScanData(results); + } + + public ParcelableScanData[] newArray(int size) { + return new ParcelableScanData[size]; + } + }; + } + + public static class ParcelableScanResults implements Parcelable { + + public ScanResult mResults[]; + + public ParcelableScanResults(ScanResult[] results) { + mResults = results; + } + + public ScanResult[] getResults() { + return mResults; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public void writeToParcel(Parcel dest, int flags) { + if (mResults != null) { + dest.writeInt(mResults.length); + for (int i = 0; i < mResults.length; i++) { + ScanResult result = mResults[i]; + result.writeToParcel(dest, flags); + } + } else { + dest.writeInt(0); + } + } + + /** + * Implement the Parcelable interface {@hide} + */ + public static final @NonNull Creator CREATOR = + new Creator() { + public ParcelableScanResults createFromParcel(Parcel in) { + throw new UnsupportedOperationException(); + } + + public ParcelableScanResults[] newArray(int size) { + return new ParcelableScanResults[size]; + } + }; + } + + /** + * {@hide} + */ + public static final String PNO_PARAMS_PNO_SETTINGS_KEY = "PnoSettings"; + /** + * {@hide} + */ + public static final String PNO_PARAMS_SCAN_SETTINGS_KEY = "ScanSettings"; + + /** + * PNO scan configuration parameters to be sent to {@link #startPnoScan}. + * Note: This structure needs to be in sync with |wifi_epno_params| struct in gscan HAL API. + * {@hide} + */ + public static class PnoSettings implements Parcelable { + /** + * Pno network to be added to the PNO scan filtering. + * {@hide} + */ + public static class PnoNetwork { + /* + * Pno flags bitmask to be set in {@link #PnoNetwork.flags} + */ + /** + * Whether directed scan needs to be performed (for hidden SSIDs) + */ + public static final byte FLAG_DIRECTED_SCAN = (1 << 0); + /** + * Whether PNO event shall be triggered if the network is found on A band + */ + public static final byte FLAG_A_BAND = (1 << 1); + /** + * Whether PNO event shall be triggered if the network is found on G band + */ + public static final byte FLAG_G_BAND = (1 << 2); + /** + * Whether strict matching is required + * If required then the firmware must store the network's SSID and not just a hash + */ + public static final byte FLAG_STRICT_MATCH = (1 << 3); + /** + * If this SSID should be considered the same network as the currently connected + * one for scoring. + */ + public static final byte FLAG_SAME_NETWORK = (1 << 4); + + /* + * Code for matching the beacon AUTH IE - additional codes. Bitmask to be set in + * {@link #PnoNetwork.authBitField} + */ + /** + * Open Network + */ + public static final byte AUTH_CODE_OPEN = (1 << 0); + /** + * WPA_PSK or WPA2PSK + */ + public static final byte AUTH_CODE_PSK = (1 << 1); + /** + * any EAPOL + */ + public static final byte AUTH_CODE_EAPOL = (1 << 2); + + /** + * SSID of the network + */ + public String ssid; + /** + * Bitmask of the FLAG_XXX + */ + public byte flags = 0; + /** + * Bitmask of the ATUH_XXX + */ + public byte authBitField = 0; + /** + * frequencies on which the particular network needs to be scanned for + */ + public int[] frequencies = {}; + + /** + * default constructor for PnoNetwork + */ + public PnoNetwork(String ssid) { + this.ssid = ssid; + } + + @Override + public int hashCode() { + return Objects.hash(ssid, flags, authBitField); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof PnoNetwork)) { + return false; + } + PnoNetwork lhs = (PnoNetwork) obj; + return TextUtils.equals(this.ssid, lhs.ssid) + && this.flags == lhs.flags + && this.authBitField == lhs.authBitField; + } + } + + /** + * Connected vs Disconnected PNO flag {@hide} + */ + public boolean isConnected; + /** + * Minimum 5GHz RSSI for a BSSID to be considered + */ + public int min5GHzRssi; + /** + * Minimum 2.4GHz RSSI for a BSSID to be considered + */ + public int min24GHzRssi; + /** + * Minimum 6GHz RSSI for a BSSID to be considered + */ + public int min6GHzRssi; + /** + * Pno Network filter list + */ + public PnoNetwork[] networkList; + + /** + * Implement the Parcelable interface {@hide} + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(isConnected ? 1 : 0); + dest.writeInt(min5GHzRssi); + dest.writeInt(min24GHzRssi); + dest.writeInt(min6GHzRssi); + if (networkList != null) { + dest.writeInt(networkList.length); + for (int i = 0; i < networkList.length; i++) { + dest.writeString(networkList[i].ssid); + dest.writeByte(networkList[i].flags); + dest.writeByte(networkList[i].authBitField); + dest.writeIntArray(networkList[i].frequencies); + } + } else { + dest.writeInt(0); + } + } + + /** + * Implement the Parcelable interface {@hide} + */ + public static final @NonNull Creator CREATOR = + new Creator() { + public PnoSettings createFromParcel(Parcel in) { + PnoSettings settings = new PnoSettings(); + settings.isConnected = in.readInt() == 1; + settings.min5GHzRssi = in.readInt(); + settings.min24GHzRssi = in.readInt(); + settings.min6GHzRssi = in.readInt(); + int numNetworks = in.readInt(); + settings.networkList = new PnoNetwork[numNetworks]; + for (int i = 0; i < numNetworks; i++) { + String ssid = in.readString(); + PnoNetwork network = new PnoNetwork(ssid); + network.flags = in.readByte(); + network.authBitField = in.readByte(); + network.frequencies = in.createIntArray(); + settings.networkList[i] = network; + } + return settings; + } + + public PnoSettings[] newArray(int size) { + return new PnoSettings[size]; + } + }; + + } + + /** + * interface to get scan events on; specify this on {@link #startBackgroundScan} or + * {@link #startScan} + */ + public interface ScanListener extends ActionListener { + /** + * Framework co-ordinates scans across multiple apps; so it may not give exactly the + * same period requested. If period of a scan is changed; it is reported by this event. + * + * @deprecated Background scan support has always been hardware vendor dependent. This + * support may not be present on newer devices. Use {@link #startScan(ScanSettings, + * ScanListener)} instead for single scans. + */ + @Deprecated + public void onPeriodChanged(int periodInMs); + + /** + * reports results retrieved from background scan and single shot scans + */ + public void onResults(ScanData[] results); + + /** + * reports full scan result for each access point found in scan + */ + public void onFullResult(ScanResult fullScanResult); + } + + /** + * interface to get PNO scan events on; specify this on {@link #startDisconnectedPnoScan} and + * {@link #startConnectedPnoScan}. + * {@hide} + */ + public interface PnoScanListener extends ScanListener { + /** + * Invoked when one of the PNO networks are found in scan results. + */ + void onPnoNetworkFound(ScanResult[] results); + } + + /** + * Enable/Disable wifi scanning. + * + * @param enable set to true to enable scanning, set to false to disable all types of scanning. + * @see WifiManager#ACTION_WIFI_SCAN_AVAILABILITY_CHANGED + * {@hide} + */ + public void setScanningEnabled(boolean enable) { + throw new UnsupportedOperationException(); + } + + /** + * Register a listener that will receive results from all single scans. + * Either the {@link ScanListener#onSuccess()} or {@link ScanListener#onFailure(int, String)} + * method will be called once when the listener is registered. + * Afterwards (assuming onSuccess was called), all subsequent single scan results will be + * delivered to the listener. It is possible that onFullResult will not be called for all + * results of the first scan if the listener was registered during the scan. + * + * @param executor the Executor on which to run the callback. + * @param listener specifies the object to report events to. This object is also treated as a + * key for this request, and must also be specified to cancel the request. + * Multiple requests should also not share this object. + * @throws SecurityException if the caller does not have permission. + */ + @RequiresPermission(anyOf = { + Manifest.permission.LOCATION_HARDWARE}) + public void registerScanListener(@NonNull Executor executor, + @NonNull ScanListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * Overload of {@link #registerScanListener(Executor, ScanListener)} that executes the callback + * synchronously. + * + * @hide + */ + public void registerScanListener(@NonNull ScanListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * Deregister a listener for ongoing single scans + * + * @param listener specifies which scan to cancel; must be same object as passed in {@link + * #registerScanListener} + */ + public void unregisterScanListener(@NonNull ScanListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * Check whether the Wi-Fi subsystem has started a scan and is waiting for scan results. + * + * @return true if a scan initiated via + * {@link WifiScanner#startScan(ScanSettings, ScanListener)} or + * {@link WifiManager#startScan()} is in progress. + * false if there is currently no scanning initiated by {@link WifiScanner} or + * {@link WifiManager}, but it's still possible the wifi radio is scanning for + * another reason. + * @hide + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public boolean isScanning() { + throw new UnsupportedOperationException(); + } + + /** + * start wifi scan in background + * + * @param settings specifies various parameters for the scan; for more information look at + * {@link ScanSettings} + * @param listener specifies the object to report events to. This object is also treated as a + * key for this scan, and must also be specified to cancel the scan. Multiple + * scans should also not share this object. + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public void startBackgroundScan(ScanSettings settings, ScanListener listener) { + startBackgroundScan(settings, listener, null); + } + + /** + * start wifi scan in background + * + * @param settings specifies various parameters for the scan; for more information look at + * {@link ScanSettings} + * @param workSource WorkSource to blame for power usage + * @param listener specifies the object to report events to. This object is also treated as a + * key for this scan, and must also be specified to cancel the scan. Multiple + * scans should also not share this object. + * @deprecated Background scan support has always been hardware vendor dependent. This support + * may not be present on newer devices. Use {@link #startScan(ScanSettings, ScanListener)} + * instead for single scans. + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public void startBackgroundScan(ScanSettings settings, ScanListener listener, + WorkSource workSource) { + throw new UnsupportedOperationException(); + } + + /** + * stop an ongoing wifi scan + * + * @param listener specifies which scan to cancel; must be same object as passed in {@link + * #startBackgroundScan} + * @deprecated Background scan support has always been hardware vendor dependent. This support + * may not be present on newer devices. Use {@link #startScan(ScanSettings, ScanListener)} + * instead for single scans. + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public void stopBackgroundScan(ScanListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * reports currently available scan results on appropriate listeners + * + * @return true if all scan results were reported correctly + * @deprecated Background scan support has always been hardware vendor dependent. This support + * may not be present on newer devices. Use {@link #startScan(ScanSettings, ScanListener)} + * instead for single scans. + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public boolean getScanResults() { + throw new UnsupportedOperationException(); + } + + /** + * starts a single scan and reports results asynchronously + * + * @param settings specifies various parameters for the scan; for more information look at + * {@link ScanSettings} + * @param listener specifies the object to report events to. This object is also treated as a + * key for this scan, and must also be specified to cancel the scan. Multiple + * scans should also not share this object. + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public void startScan(ScanSettings settings, ScanListener listener) { + startScan(settings, listener, null); + } + + /** + * starts a single scan and reports results asynchronously + * + * @param settings specifies various parameters for the scan; for more information look at + * {@link ScanSettings} + * @param listener specifies the object to report events to. This object is also treated as a + * key for this scan, and must also be specified to cancel the scan. Multiple + * scans should also not share this object. + * @param workSource WorkSource to blame for power usage + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public void startScan(ScanSettings settings, ScanListener listener, WorkSource workSource) { + startScan(settings, null, listener, workSource); + } + + /** + * starts a single scan and reports results asynchronously + * + * @param settings specifies various parameters for the scan; for more information look at + * {@link ScanSettings} + * @param executor the Executor on which to run the callback. + * @param listener specifies the object to report events to. This object is also treated as a + * key for this scan, and must also be specified to cancel the scan. Multiple + * scans should also not share this object. + * @param workSource WorkSource to blame for power usage + * @hide + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public void startScan(ScanSettings settings, @Nullable Executor executor, + ScanListener listener, WorkSource workSource) { + throw new UnsupportedOperationException(); + } + + /** + * stops an ongoing single shot scan; only useful after {@link #startScan} if onResults() + * hasn't been called on the listener, ignored otherwise + * + * @param listener + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public void stopScan(ScanListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * Retrieve the most recent scan results from a single scan request. + */ + @NonNull + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public List getSingleScanResults() { + throw new UnsupportedOperationException(); + } + + private void startPnoScan(ScanSettings scanSettings, PnoSettings pnoSettings, int key) { + throw new UnsupportedOperationException(); + } + + /** + * Start wifi connected PNO scan + * + * @param scanSettings specifies various parameters for the scan; for more information look at + * {@link ScanSettings} + * @param pnoSettings specifies various parameters for PNO; for more information look at + * {@link PnoSettings} + * @param executor the Executor on which to run the callback. + * @param listener specifies the object to report events to. This object is also treated as a + * key for this scan, and must also be specified to cancel the scan. Multiple + * scans should also not share this object. + * {@hide} + */ + public void startConnectedPnoScan(ScanSettings scanSettings, PnoSettings pnoSettings, + @NonNull Executor executor, PnoScanListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * Start wifi disconnected PNO scan + * + * @param scanSettings specifies various parameters for the scan; for more information look at + * {@link ScanSettings} + * @param pnoSettings specifies various parameters for PNO; for more information look at + * {@link PnoSettings} + * @param listener specifies the object to report events to. This object is also treated as a + * key for this scan, and must also be specified to cancel the scan. Multiple + * scans should also not share this object. + * {@hide} + */ + public void startDisconnectedPnoScan(ScanSettings scanSettings, PnoSettings pnoSettings, + @NonNull Executor executor, PnoScanListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * Stop an ongoing wifi PNO scan + * + * @param listener specifies which scan to cancel; must be same object as passed in {@link + * #startPnoScan} + * {@hide} + */ + public void stopPnoScan(ScanListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * specifies information about an access point of interest + */ + @Deprecated + public static class BssidInfo { + /** + * bssid of the access point; in XX:XX:XX:XX:XX:XX format + */ + public String bssid; + /** + * low signal strength threshold; more information at {@link ScanResult#level} + */ + public int low; /* minimum RSSI */ + /** + * high signal threshold; more information at {@link ScanResult#level} + */ + public int high; /* maximum RSSI */ + /** + * channel frequency (in KHz) where you may find this BSSID + */ + public int frequencyHint; + } + + /** + * @hide + */ + @Deprecated + public static class WifiChangeSettings implements Parcelable { + public int rssiSampleSize; /* sample size for RSSI averaging */ + public int lostApSampleSize; /* samples to confirm AP's loss */ + public int unchangedSampleSize; /* samples to confirm no change */ + public int minApsBreachingThreshold; /* change threshold to trigger event */ + public int periodInMs; /* scan period in millisecond */ + public BssidInfo[] bssidInfos; + + /** + * Implement the Parcelable interface {@hide} + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public void writeToParcel(Parcel dest, int flags) { + } + + /** + * Implement the Parcelable interface {@hide} + */ + public static final @NonNull Creator CREATOR = + new Creator() { + public WifiChangeSettings createFromParcel(Parcel in) { + return new WifiChangeSettings(); + } + + public WifiChangeSettings[] newArray(int size) { + return new WifiChangeSettings[size]; + } + }; + + } + + /** + * configure WifiChange detection + * + * @param rssiSampleSize number of samples used for RSSI averaging + * @param lostApSampleSize number of samples to confirm an access point's loss + * @param unchangedSampleSize number of samples to confirm there are no changes + * @param minApsBreachingThreshold minimum number of access points that need to be + * out of range to detect WifiChange + * @param periodInMs indicates period of scan to find changes + * @param bssidInfos access points to watch + */ + @Deprecated + @SuppressLint("RequiresPermission") + public void configureWifiChange( + int rssiSampleSize, /* sample size for RSSI averaging */ + int lostApSampleSize, /* samples to confirm AP's loss */ + int unchangedSampleSize, /* samples to confirm no change */ + int minApsBreachingThreshold, /* change threshold to trigger event */ + int periodInMs, /* period of scan */ + BssidInfo[] bssidInfos /* signal thresholds to cross */ + ) { + throw new UnsupportedOperationException(); + } + + /** + * interface to get wifi change events on; use this on {@link #startTrackingWifiChange} + */ + @Deprecated + public interface WifiChangeListener extends ActionListener { + /** + * indicates that changes were detected in wifi environment + * + * @param results indicate the access points that exhibited change + */ + public void onChanging(ScanResult[] results); /* changes are found */ + + /** + * indicates that no wifi changes are being detected for a while + * + * @param results indicate the access points that are bing monitored for change + */ + public void onQuiescence(ScanResult[] results); /* changes settled down */ + } + + /** + * track changes in wifi environment + * + * @param listener object to report events on; this object must be unique and must also be + * provided on {@link #stopTrackingWifiChange} + */ + @Deprecated + @SuppressLint("RequiresPermission") + public void startTrackingWifiChange(WifiChangeListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * stop tracking changes in wifi environment + * + * @param listener object that was provided to report events on {@link + * #stopTrackingWifiChange} + */ + @Deprecated + @SuppressLint("RequiresPermission") + public void stopTrackingWifiChange(WifiChangeListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * @hide + */ + @Deprecated + @SuppressLint("RequiresPermission") + public void configureWifiChange(WifiChangeSettings settings) { + throw new UnsupportedOperationException(); + } + + @Deprecated + public static interface BssidListener extends ActionListener { + /** + * indicates that access points were found by on going scans + * + * @param results list of scan results, one for each access point visible currently + */ + public void onFound(ScanResult[] results); + + /** + * indicates that access points were missed by on going scans + * + * @param results list of scan results, for each access point that is not visible anymore + */ + public void onLost(ScanResult[] results); + } + + /** + * @hide + */ + @Deprecated + public static class HotlistSettings implements Parcelable { + public BssidInfo[] bssidInfos; + public int apLostThreshold; + + /** + * Implement the Parcelable interface {@hide} + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public void writeToParcel(Parcel dest, int flags) { + } + + /** + * Implement the Parcelable interface {@hide} + */ + public static final @NonNull Creator CREATOR = + new Creator() { + public HotlistSettings createFromParcel(Parcel in) { + HotlistSettings settings = new HotlistSettings(); + return settings; + } + + public HotlistSettings[] newArray(int size) { + return new HotlistSettings[size]; + } + }; + } + + /** + * set interesting access points to find + * + * @param bssidInfos access points of interest + * @param apLostThreshold number of scans needed to indicate that AP is lost + * @param listener object provided to report events on; this object must be unique and must + * also be provided on {@link #stopTrackingBssids} + */ + @Deprecated + @SuppressLint("RequiresPermission") + public void startTrackingBssids(BssidInfo[] bssidInfos, + int apLostThreshold, BssidListener listener) { + throw new UnsupportedOperationException(); + } + + /** + * remove tracking of interesting access points + * + * @param listener same object provided in {@link #startTrackingBssids} + */ + @Deprecated + @SuppressLint("RequiresPermission") + public void stopTrackingBssids(BssidListener listener) { + throw new UnsupportedOperationException(); + } + + + /* private members and methods */ + + private static final String TAG = "WifiScanner"; + private static final boolean DBG = false; + + /* commands for Wifi Service */ + private static final int BASE; + + static { + if (1==1) throw new UnsupportedOperationException(); + BASE = 0; + } + + /** + * @hide + */ + public static final int CMD_START_BACKGROUND_SCAN = BASE + 2; + /** + * @hide + */ + public static final int CMD_STOP_BACKGROUND_SCAN = BASE + 3; + /** + * @hide + */ + public static final int CMD_GET_SCAN_RESULTS = BASE + 4; + /** + * @hide + */ + public static final int CMD_SCAN_RESULT = BASE + 5; + /** + * @hide + */ + public static final int CMD_OP_SUCCEEDED = BASE + 17; + /** + * @hide + */ + public static final int CMD_OP_FAILED = BASE + 18; + /** + * @hide + */ + public static final int CMD_FULL_SCAN_RESULT = BASE + 20; + /** + * @hide + */ + public static final int CMD_START_SINGLE_SCAN = BASE + 21; + /** + * @hide + */ + public static final int CMD_STOP_SINGLE_SCAN = BASE + 22; + /** + * @hide + */ + public static final int CMD_SINGLE_SCAN_COMPLETED = BASE + 23; + /** + * @hide + */ + public static final int CMD_START_PNO_SCAN = BASE + 24; + /** + * @hide + */ + public static final int CMD_STOP_PNO_SCAN = BASE + 25; + /** + * @hide + */ + public static final int CMD_PNO_NETWORK_FOUND = BASE + 26; + /** + * @hide + */ + public static final int CMD_REGISTER_SCAN_LISTENER = BASE + 27; + /** + * @hide + */ + public static final int CMD_DEREGISTER_SCAN_LISTENER = BASE + 28; + /** + * @hide + */ + public static final int CMD_GET_SINGLE_SCAN_RESULTS = BASE + 29; + /** + * @hide + */ + public static final int CMD_ENABLE = BASE + 30; + /** + * @hide + */ + public static final int CMD_DISABLE = BASE + 31; + + private Context mContext; + + private static final int INVALID_KEY = 0; + private int mListenerKey = 1; + + private final SparseArray mListenerMap = new SparseArray(); + private final SparseArray mExecutorMap = new SparseArray<>(); + private final Object mListenerMapLock = new Object(); + + + + // Add a listener into listener map. If the listener already exists, return INVALID_KEY and + // send an error message to internal handler; Otherwise add the listener to the listener map and + // return the key of the listener. + private int addListener(ActionListener listener, Executor executor) { + throw new UnsupportedOperationException(); + } + + private int putListener(Object listener) { + if (listener == null) return INVALID_KEY; + int key; + synchronized (mListenerMapLock) { + do { + key = mListenerKey++; + } while (key == INVALID_KEY); + mListenerMap.put(key, listener); + } + return key; + } + + private static class ListenerWithExecutor { + @Nullable + final Object mListener; + @Nullable + final Executor mExecutor; + + ListenerWithExecutor(@Nullable Object listener, @Nullable Executor executor) { + mListener = listener; + mExecutor = executor; + } + } + + private ListenerWithExecutor getListenerWithExecutor(int key) { + if (key == INVALID_KEY) return new ListenerWithExecutor(null, null); + synchronized (mListenerMapLock) { + Object listener = mListenerMap.get(key); + Executor executor = mExecutorMap.get(key); + return new ListenerWithExecutor(listener, executor); + } + } + + private int getListenerKey(Object listener) { + if (listener == null) return INVALID_KEY; + synchronized (mListenerMapLock) { + int index = mListenerMap.indexOfValue(listener); + if (index == -1) { + return INVALID_KEY; + } else { + return mListenerMap.keyAt(index); + } + } + } + + private Object removeListener(int key) { + if (key == INVALID_KEY) return null; + synchronized (mListenerMapLock) { + Object listener = mListenerMap.get(key); + mListenerMap.remove(key); + mExecutorMap.remove(key); + return listener; + } + } + + private int removeListener(Object listener) { + int key = getListenerKey(listener); + if (key == INVALID_KEY) { + Log.e(TAG, "listener cannot be found"); + return key; + } + synchronized (mListenerMapLock) { + mListenerMap.remove(key); + mExecutorMap.remove(key); + return key; + } + } + + /** + * @hide + */ + @VisibleForTesting + public Handler getInternalHandler() { + throw new UnsupportedOperationException(); + } + + /** + * @hide + */ + public static class OperationResult implements Parcelable { + public int reason; + public String description; + + public OperationResult(int reason, String description) { + this.reason = reason; + this.description = description; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface {@hide} + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(reason); + dest.writeString(description); + } + + /** + * Implement the Parcelable interface {@hide} + */ + public static final @NonNull Creator CREATOR = + new Creator() { + public OperationResult createFromParcel(Parcel in) { + int reason = in.readInt(); + String description = in.readString(); + return new OperationResult(reason, description); + } + + public OperationResult[] newArray(int size) { + return new OperationResult[size]; + } + }; + } + + private class ServiceHandler extends Handler { + ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + throw new UnsupportedOperationException(); + } + } +} \ No newline at end of file diff --git a/play-services-location/system-api/src/main/java/com/android/internal/location/ProviderProperties.java b/play-services-location/system-api/src/main/java/com/android/internal/location/ProviderProperties.java new file mode 100644 index 0000000000..4d975d3a51 --- /dev/null +++ b/play-services-location/system-api/src/main/java/com/android/internal/location/ProviderProperties.java @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2021 The Android Open Source Project + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.internal.location; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A Parcelable containing (legacy) location provider properties. + * This object is just used inside the framework and system services. + * + * @hide + */ +public final class ProviderProperties implements Parcelable { + /** + * True if provider requires access to a + * data network (e.g., the Internet), false otherwise. + */ + public final boolean mRequiresNetwork; + + /** + * True if the provider requires access to a + * satellite-based positioning system (e.g., GPS), false + * otherwise. + */ + public final boolean mRequiresSatellite; + + /** + * True if the provider requires access to an appropriate + * cellular network (e.g., to make use of cell tower IDs), false + * otherwise. + */ + public final boolean mRequiresCell; + + /** + * True if the use of this provider may result in a + * monetary charge to the user, false if use is free. It is up to + * each provider to give accurate information. Cell (network) usage + * is not considered monetary cost. + */ + public final boolean mHasMonetaryCost; + + /** + * True if the provider is able to provide altitude + * information, false otherwise. A provider that reports altitude + * under most circumstances but may occasionally not report it + * should return true. + */ + public final boolean mSupportsAltitude; + + /** + * True if the provider is able to provide speed + * information, false otherwise. A provider that reports speed + * under most circumstances but may occasionally not report it + * should return true. + */ + public final boolean mSupportsSpeed; + + /** + * True if the provider is able to provide bearing + * information, false otherwise. A provider that reports bearing + * under most circumstances but may occasionally not report it + * should return true. + */ + public final boolean mSupportsBearing; + + /** + * Power requirement for this provider. + * + * @return the power requirement for this provider, as one of the + * constants Criteria.POWER_*. + */ + public final int mPowerRequirement; + + /** + * Constant describing the horizontal accuracy returned + * by this provider. + * + * @return the horizontal accuracy for this provider, as one of the + * constants Criteria.ACCURACY_COARSE or Criteria.ACCURACY_FINE + */ + public final int mAccuracy; + + public ProviderProperties(boolean mRequiresNetwork, + boolean mRequiresSatellite, boolean mRequiresCell, boolean mHasMonetaryCost, + boolean mSupportsAltitude, boolean mSupportsSpeed, boolean mSupportsBearing, + int mPowerRequirement, int mAccuracy) { + this.mRequiresNetwork = mRequiresNetwork; + this.mRequiresSatellite = mRequiresSatellite; + this.mRequiresCell = mRequiresCell; + this.mHasMonetaryCost = mHasMonetaryCost; + this.mSupportsAltitude = mSupportsAltitude; + this.mSupportsSpeed = mSupportsSpeed; + this.mSupportsBearing = mSupportsBearing; + this.mPowerRequirement = mPowerRequirement; + this.mAccuracy = mAccuracy; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ProviderProperties createFromParcel(Parcel in) { + return null; + } + + @Override + public ProviderProperties[] newArray(int size) { + return null; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + } +} diff --git a/play-services-location/system-api/src/main/java/com/android/internal/location/ProviderRequest.java b/play-services-location/system-api/src/main/java/com/android/internal/location/ProviderRequest.java new file mode 100644 index 0000000000..1507497978 --- /dev/null +++ b/play-services-location/system-api/src/main/java/com/android/internal/location/ProviderRequest.java @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2012, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.internal.location; + +import android.location.LocationRequest; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.List; + +/** + * @hide + */ +public final class ProviderRequest implements Parcelable { + /** + * Location reporting is requested (true) + */ + public boolean reportLocation = false; + + /** + * The smallest requested interval + */ + public long interval = Long.MAX_VALUE; + + /** + * A more detailed set of requests. + *

    Location Providers can optionally use this to + * fine tune location updates, for example when there + * is a high power slow interval request and a + * low power fast interval request. + */ + public List locationRequests = null; + + public ProviderRequest() { + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ProviderRequest createFromParcel(Parcel in) { + return null; + } + + @Override + public ProviderRequest[] newArray(int size) { + return null; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + } +} diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/GeocodeProvider.java b/play-services-location/system-api/src/main/java/com/android/location/provider/GeocodeProvider.java new file mode 100644 index 0000000000..e5100b8666 --- /dev/null +++ b/play-services-location/system-api/src/main/java/com/android/location/provider/GeocodeProvider.java @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2010, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.location.provider; + +import android.location.Address; +import android.location.GeocoderParams; +import android.os.IBinder; + +import java.util.List; + +/** + * Base class for geocode providers implemented as unbundled services. + *

    + *

    Geocode providers can be implemented as services and return the result of + * {@link GeocodeProvider#getBinder()} in its getBinder() method. + *

    + *

    IMPORTANT: This class is effectively a public API for unbundled + * applications, and must remain API stable. See README.txt in the root + * of this package for more information. + */ +public abstract class GeocodeProvider { + /** + * This method is overridden to implement the + * {@link android.location.Geocoder#getFromLocation(double, double, int)} method. + * Classes implementing this method should not hold a reference to the params parameter. + */ + public abstract String onGetFromLocation(double latitude, double longitude, int maxResults, + GeocoderParams params, List

    addrs); + + /** + * This method is overridden to implement the + * {@link android.location.Geocoder#getFromLocationName(String, int, double, double, double, double)} method. + * Classes implementing this method should not hold a reference to the params parameter. + */ + public abstract String onGetFromLocationName(String locationName, + double lowerLeftLatitude, double lowerLeftLongitude, + double upperRightLatitude, double upperRightLongitude, int maxResults, + GeocoderParams params, List
    addrs); + + /** + * Returns the Binder interface for the geocode provider. + * This is intended to be used for the onBind() method of + * a service that implements a geocoder service. + * + * @return the IBinder instance for the provider + */ + public IBinder getBinder() { + return null; + } +} diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/LocationProvider.java b/play-services-location/system-api/src/main/java/com/android/location/provider/LocationProvider.java new file mode 100644 index 0000000000..c97ad80480 --- /dev/null +++ b/play-services-location/system-api/src/main/java/com/android/location/provider/LocationProvider.java @@ -0,0 +1,232 @@ +/* + * SPDX-FileCopyrightText: 2010, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.location.provider; + +import android.location.Criteria; +import android.location.Location; +import android.os.Bundle; +import android.os.IBinder; +import android.os.WorkSource; + +/** + * An abstract superclass for location providers that are implemented + * outside of the core android platform. + * Location providers can be implemented as services and return the result of + * {@link LocationProvider#getBinder()} in its getBinder() method. + * + * @hide + */ +public abstract class LocationProvider { + + public LocationProvider() { + } + + /** + * Returns the Binder interface for the location provider. + * This is intended to be used for the onBind() method of + * a service that implements a location provider service. + * + * @return the IBinder instance for the provider + */ + public IBinder getBinder() { + return null; + } + + /** + * Used by the location provider to report new locations. + * + * @param location new Location to report + *

    + * Requires the android.permission.INSTALL_LOCATION_PROVIDER permission. + */ + public void reportLocation(Location location) { + } + + /** + * Returns true if the provider requires access to a + * data network (e.g., the Internet), false otherwise. + */ + public abstract boolean onRequiresNetwork(); + + /** + * Returns true if the provider requires access to a + * satellite-based positioning system (e.g., GPS), false + * otherwise. + */ + public abstract boolean onRequiresSatellite(); + + /** + * Returns true if the provider requires access to an appropriate + * cellular network (e.g., to make use of cell tower IDs), false + * otherwise. + */ + public abstract boolean onRequiresCell(); + + /** + * Returns true if the use of this provider may result in a + * monetary charge to the user, false if use is free. It is up to + * each provider to give accurate information. + */ + public abstract boolean onHasMonetaryCost(); + + /** + * Returns true if the provider is able to provide altitude + * information, false otherwise. A provider that reports altitude + * under most circumstances but may occassionally not report it + * should return true. + */ + public abstract boolean onSupportsAltitude(); + + /** + * Returns true if the provider is able to provide speed + * information, false otherwise. A provider that reports speed + * under most circumstances but may occassionally not report it + * should return true. + */ + public abstract boolean onSupportsSpeed(); + + /** + * Returns true if the provider is able to provide bearing + * information, false otherwise. A provider that reports bearing + * under most circumstances but may occassionally not report it + * should return true. + */ + public abstract boolean onSupportsBearing(); + + /** + * Returns the power requirement for this provider. + * + * @return the power requirement for this provider, as one of the + * constants Criteria.POWER_REQUIREMENT_*. + */ + public abstract int onGetPowerRequirement(); + + /** + * Returns true if this provider meets the given criteria, + * false otherwise. + */ + public abstract boolean onMeetsCriteria(Criteria criteria); + + /** + * Returns a constant describing horizontal accuracy of this provider. + * If the provider returns finer grain or exact location, + * {@link Criteria#ACCURACY_FINE} is returned, otherwise if the + * location is only approximate then {@link Criteria#ACCURACY_COARSE} + * is returned. + */ + public abstract int onGetAccuracy(); + + /** + * Enables the location provider + */ + public abstract void onEnable(); + + /** + * Disables the location provider + */ + public abstract void onDisable(); + + /** + * Returns a information on the status of this provider. + * {@link android.location.LocationProvider#OUT_OF_SERVICE} is returned if the provider is + * out of service, and this is not expected to change in the near + * future; {@link android.location.LocationProvider#TEMPORARILY_UNAVAILABLE} is returned if + * the provider is temporarily unavailable but is expected to be + * available shortly; and {@link android.location.LocationProvider#AVAILABLE} is returned + * if the provider is currently available. + *

    + *

    If extras is non-null, additional status information may be + * added to it in the form of provider-specific key/value pairs. + */ + public abstract int onGetStatus(Bundle extras); + + /** + * Returns the time at which the status was last updated. It is the + * responsibility of the provider to appropriately set this value using + * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()}. + * there is a status update that it wishes to broadcast to all its + * listeners. The provider should be careful not to broadcast + * the same status again. + * + * @return time of last status update in millis since last reboot + */ + public abstract long onGetStatusUpdateTime(); + + /** + * Returns debugging information about the location provider. + * + * @return string describing the internal state of the location provider, or null. + */ + public abstract String onGetInternalState(); + + /** + * Notifies the location provider that clients are listening for locations. + * Called with enable set to true when the first client is added and + * called with enable set to false when the last client is removed. + * This allows the provider to prepare for receiving locations, + * and to shut down when no clients are remaining. + * + * @param enable true if location tracking should be enabled. + */ + public abstract void onEnableLocationTracking(boolean enable); + + /** + * Notifies the location provider of the smallest minimum time between updates amongst + * all clients that are listening for locations. This allows the provider to reduce + * the frequency of updates to match the requested frequency. + * + * @param minTime the smallest minTime value over all listeners for this provider. + * @param ws the source this work is coming from. + */ + public abstract void onSetMinTime(long minTime, WorkSource ws); + + /** + * Updates the network state for the given provider. This function must + * be overwritten if {@link android.location.LocationProvider#requiresNetwork} returns true. + * The state is {@link android.location.LocationProvider#TEMPORARILY_UNAVAILABLE} (disconnected) + * OR {@link android.location.LocationProvider#AVAILABLE} (connected or connecting). + * + * @param state data state + */ + @SuppressWarnings("deprecation") + public abstract void onUpdateNetworkState(int state, android.net.NetworkInfo info); + + /** + * Informs the provider when a new location has been computed by a different + * location provider. This is intended to be used as aiding data for the + * receiving provider. + * + * @param location new location from other location provider + */ + public abstract void onUpdateLocation(Location location); + + /** + * Implements addditional location provider specific additional commands. + * + * @param command name of the command to send to the provider. + * @param extras optional arguments for the command (or null). + * The provider may optionally fill the extras Bundle with results from the command. + * @return true if the command succeeds. + */ + public abstract boolean onSendExtraCommand(String command, Bundle extras); + + /** + * Notifies the location provider when a new client is listening for locations. + * + * @param uid user ID of the new client. + * @param ws a WorkSource representation of the client. + */ + public abstract void onAddListener(int uid, WorkSource ws); + + /** + * Notifies the location provider when a client is no longer listening for locations. + * + * @param uid user ID of the client no longer listening. + * @param ws a WorkSource representation of the client. + */ + public abstract void onRemoveListener(int uid, WorkSource ws); +} diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/LocationProviderBase.java b/play-services-location/system-api/src/main/java/com/android/location/provider/LocationProviderBase.java new file mode 100644 index 0000000000..888917cf5b --- /dev/null +++ b/play-services-location/system-api/src/main/java/com/android/location/provider/LocationProviderBase.java @@ -0,0 +1,276 @@ +/* + * SPDX-FileCopyrightText: 2010, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.location.provider; + +import android.content.Context; +import android.location.Location; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.location.provider.ProviderProperties; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.IBinder; +import android.os.WorkSource; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.List; + +/** + * Base class for location providers implemented as unbundled services. + * + *

    The network location provider must export a service with action + * "com.android.location.service.v2.NetworkLocationProvider" + * and a valid minor version in a meta-data field on the service, and + * then return the result of {@link #getBinder()} on service binding. + * + *

    The fused location provider must export a service with action + * "com.android.location.service.FusedLocationProvider" + * and a valid minor version in a meta-data field on the service, and + * then return the result of {@link #getBinder()} on service binding. + * + *

    IMPORTANT: This class is effectively a public API for unbundled + * applications, and must remain API stable. See README.txt in the root + * of this package for more information. + * + * @deprecated This class is not part of the standard API surface - use + * {@link android.location.provider.LocationProviderBase} instead. + */ +@Deprecated +public abstract class LocationProviderBase { + + /** + * Callback to be invoked when a flush operation is complete and all flushed locations have been + * reported. + */ + protected interface OnFlushCompleteCallback { + + /** + * Should be invoked once the flush is complete. + */ + void onFlushComplete(); + } + + /** + * Bundle key for a version of the location containing no GPS data. + * Allows location providers to flag locations as being safe to + * feed to LocationFudger. + * + * @deprecated Do not use from Android R onwards. + */ + @Deprecated + public static final String EXTRA_NO_GPS_LOCATION = "noGPSLocation"; + + /** + * Name of the Fused location provider. + * + *

    This provider combines inputs for all possible location sources + * to provide the best possible Location fix. + */ + public static final String FUSED_PROVIDER = LocationManager.FUSED_PROVIDER; + + final String mTag; + @Nullable + final String mAttributionTag; + final IBinder mBinder; + + + volatile ProviderProperties mProperties; + volatile boolean mAllowed; + + /** + * @deprecated Prefer + * {@link #LocationProviderBase(Context, String, ProviderPropertiesUnbundled)}. + */ + @Deprecated + public LocationProviderBase(String tag, ProviderPropertiesUnbundled properties) { + throw new UnsupportedOperationException(); + } + + /** + * This constructor associates the feature id of the given context with this location provider. + * The location service may afford special privileges to incoming calls identified as belonging + * to this location provider. + */ + @RequiresApi(VERSION_CODES.R) + public LocationProviderBase(Context context, String tag, + ProviderPropertiesUnbundled properties) { + throw new UnsupportedOperationException(); + } + + public IBinder getBinder() { + return mBinder; + } + + /** + * @deprecated Use {@link #setAllowed(boolean)} instead. + */ + @Deprecated + @RequiresApi(VERSION_CODES.Q) + public void setEnabled(boolean enabled) { + throw new UnsupportedOperationException(); + } + + /** + * Sets whether this provider is currently allowed or not. Note that this is specific to the + * provider only, and is not related to global location settings. This is a hint to the Location + * Manager that this provider will generally be unable to fulfill incoming requests. This + * provider may still receive callbacks to onSetRequest while not allowed, and must decide + * whether to attempt to satisfy those requests or not. + * + *

    Some guidelines: providers should set their own allowed/disallowed status based only on + * state "owned" by that provider. For instance, providers should not take into account the + * state of the location master setting when setting themselves allowed or disallowed, as this + * state is not owned by a particular provider. If a provider requires some additional user + * consent that is particular to the provider, this should be use to set the allowed/disallowed + * state. If the provider proxies to another provider, the child provider's allowed/disallowed + * state should be taken into account in the parent's allowed state. For most providers, it is + * expected that they will be always allowed. + */ + @RequiresApi(VERSION_CODES.R) + public void setAllowed(boolean allowed) { + throw new UnsupportedOperationException(); + } + + /** + * Sets the provider properties that may be queried by clients. Generally speaking, providers + * should try to avoid changing their properties after construction. + */ + @RequiresApi(VERSION_CODES.Q) + public void setProperties(ProviderPropertiesUnbundled properties) { + throw new UnsupportedOperationException(); + } + + /** + * Sets a list of additional packages that should be considered as part of this location + * provider for the purposes of generating locations. This should generally only be used when + * another package may issue location requests on behalf of this package in the course of + * providing location. This will inform location services to treat the other packages as + * location providers as well. + * + * @deprecated On Android R and above this has no effect. + */ + @Deprecated + @RequiresApi(VERSION_CODES.Q) + public void setAdditionalProviderPackages(List packageNames) {} + + /** + * @deprecated Use {@link #isAllowed()} instead. + */ + @Deprecated + @RequiresApi(VERSION_CODES.Q) + public boolean isEnabled() { + throw new UnsupportedOperationException(); + } + + /** + * Returns true if this provider is allowed. Providers start as allowed on construction. + */ + @RequiresApi(VERSION_CODES.R) + public boolean isAllowed() { + throw new UnsupportedOperationException(); + } + + /** + * Reports a new location from this provider. + */ + public void reportLocation(@NonNull Location location) { + throw new UnsupportedOperationException(); + } + + /** + * Reports a new batch of locations from this provider. Locations must be ordered in the list + * from earliest first to latest last. + */ + public void reportLocations(@NonNull List locations) { + throw new UnsupportedOperationException(); + } + + protected void onInit() { + // call once so that providers designed for APIs pre-Q are not broken + throw new UnsupportedOperationException(); + } + + /** + * @deprecated This callback will be invoked once when the provider is created to maintain + * backwards compatibility with providers not designed for Android Q and above. This method + * should only be implemented in location providers that need to support SDKs below Android Q. + * Even in this case, it is usually unnecessary to implement this callback with the correct + * design. This method may be removed in the future. + */ + @Deprecated + protected void onEnable() {} + + /** + * @deprecated This callback will be never be invoked on Android Q and above. This method should + * only be implemented in location providers that need to support SDKs below Android Q. Even in + * this case, it is usually unnecessary to implement this callback with the correct design. This + * method may be removed in the future. + */ + @Deprecated + protected void onDisable() {} + + /** + * Set the {@link ProviderRequestUnbundled} requirements for this provider. Each call to this method + * overrides all previous requests. This method might trigger the provider to start returning + * locations, or to stop returning locations, depending on the parameters in the request. + */ + protected abstract void onSetRequest(ProviderRequestUnbundled request, WorkSource source); + + /** + * Requests a flush of any pending batched locations. The callback must always be invoked once + * per invocation, and should be invoked after {@link #reportLocation(Location)} or + * {@link #reportLocations(List)} has been invoked with any flushed locations. The callback may + * be invoked immediately if no locations are flushed. + */ + protected void onFlush(OnFlushCompleteCallback callback) { + callback.onFlushComplete(); + } + + /** + * @deprecated This callback will never be invoked on Android Q and above. This method may be + * removed in the future. Prefer to dump provider state via the containing service instead. + */ + @Deprecated + protected void onDump(FileDescriptor fd, PrintWriter pw, String[] args) {} + + /** + * This method will no longer be invoked. + * + * @deprecated This callback will be never be invoked on Android Q and above. This method should + * only be implemented in location providers that need to support SDKs below Android Q. This + * method may be removed in the future. + */ + @Deprecated + protected int onGetStatus(Bundle extras) { + return LocationProvider.AVAILABLE; + } + + /** + * This method will no longer be invoked. + * + * @deprecated This callback will be never be invoked on Android Q and above. This method should + * only be implemented in location providers that need to support SDKs below Android Q. This + * method may be removed in the future. + */ + @Deprecated + protected long onGetStatusUpdateTime() { + return 0; + } + + /** + * Implements location provider specific custom commands. The return value will be ignored on + * Android Q and above. + */ + protected boolean onSendExtraCommand(@Nullable String command, @Nullable Bundle extras) { + return false; + } +} \ No newline at end of file diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/LocationRequestUnbundled.java b/play-services-location/system-api/src/main/java/com/android/location/provider/LocationRequestUnbundled.java new file mode 100644 index 0000000000..aaf64aaec3 --- /dev/null +++ b/play-services-location/system-api/src/main/java/com/android/location/provider/LocationRequestUnbundled.java @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: 2012, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.location.provider; + +import android.location.LocationRequest; + +/** + * This class is an interface to LocationRequests for unbundled applications. + *

    + *

    IMPORTANT: This class is effectively a public API for unbundled + * applications, and must remain API stable. See README.txt in the root + * of this package for more information. + */ +public final class LocationRequestUnbundled { + /** + * Returned by {@link #getQuality} when requesting the most accurate locations available. + *

    + *

    This may be up to 1 meter accuracy, although this is implementation dependent. + */ + public static final int ACCURACY_FINE = LocationRequest.ACCURACY_FINE; + + /** + * Returned by {@link #getQuality} when requesting "block" level accuracy. + *

    + *

    Block level accuracy is considered to be about 100 meter accuracy, + * although this is implementation dependent. Using a coarse accuracy + * such as this often consumes less power. + */ + public static final int ACCURACY_BLOCK = LocationRequest.ACCURACY_BLOCK; + + /** + * Returned by {@link #getQuality} when requesting "city" level accuracy. + *

    + *

    City level accuracy is considered to be about 10km accuracy, + * although this is implementation dependent. Using a coarse accuracy + * such as this often consumes less power. + */ + public static final int ACCURACY_CITY = LocationRequest.ACCURACY_CITY; + + /** + * Returned by {@link #getQuality} when requiring no direct power impact (passive locations). + *

    + *

    This location request will not trigger any active location requests, + * but will receive locations triggered by other applications. Your application + * will not receive any direct power blame for location work. + */ + public static final int POWER_NONE = LocationRequest.POWER_NONE; + + /** + * Returned by {@link #getQuality} when requesting low power impact. + *

    + *

    This location request will avoid high power location work where + * possible. + */ + public static final int POWER_LOW = LocationRequest.POWER_LOW; + + /** + * Returned by {@link #getQuality} when allowing high power consumption for location. + *

    + *

    This location request will allow high power location work. + */ + public static final int POWER_HIGH = LocationRequest.POWER_HIGH; + + /** + * Get the desired interval of this request, in milliseconds. + * + * @return desired interval in milliseconds, inexact + */ + public long getInterval() { + return 0; + } + + /** + * Get the fastest interval of this request, in milliseconds. + *

    + *

    The system will never provide location updates faster + * than the minimum of {@link #getFastestInterval} and + * {@link #getInterval}. + * + * @return fastest interval in milliseconds, exact + */ + public long getFastestInterval() { + return 0; + } + + /** + * Get the quality of the request. + * + * @return an accuracy or power constant + */ + public int getQuality() { + return 0; + } + + /** + * Get the minimum distance between location updates, in meters. + * + * @return minimum distance between location updates in meters + */ + public float getSmallestDisplacement() { + return 0; + } +} diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/ProviderPropertiesUnbundled.java b/play-services-location/system-api/src/main/java/com/android/location/provider/ProviderPropertiesUnbundled.java new file mode 100644 index 0000000000..2742fe410f --- /dev/null +++ b/play-services-location/system-api/src/main/java/com/android/location/provider/ProviderPropertiesUnbundled.java @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2012, The Android Open Source Project + * SPDX-FileCopyrightText: 2014, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.location.provider; + +import com.android.internal.location.ProviderProperties; + +/** + * This class is an interface to Provider Properties for unbundled applications. + *

    + *

    IMPORTANT: This class is effectively a public API for unbundled + * applications, and must remain API stable. See README.txt in the root + * of this package for more information. + */ +public final class ProviderPropertiesUnbundled { + public static ProviderPropertiesUnbundled create(boolean requiresNetwork, + boolean requiresSatellite, boolean requiresCell, boolean hasMonetaryCost, + boolean supportsAltitude, boolean supportsSpeed, boolean supportsBearing, + int powerRequirement, int accuracy) { + return null; + } + + public ProviderProperties getProviderProperties() { + return null; + } +} diff --git a/play-services-location/system-api/src/main/java/com/android/location/provider/ProviderRequestUnbundled.java b/play-services-location/system-api/src/main/java/com/android/location/provider/ProviderRequestUnbundled.java new file mode 100644 index 0000000000..2b96e0ecbd --- /dev/null +++ b/play-services-location/system-api/src/main/java/com/android/location/provider/ProviderRequestUnbundled.java @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2012 The Android Open Source Project + * SPDX-FileCopyrightText: 2014 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.location.provider; + +import android.os.Build; +import android.os.WorkSource; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import com.android.internal.location.ProviderRequest; + +import java.util.List; + +/** + * This class is an interface to Provider Requests for unbundled applications. + *

    + *

    IMPORTANT: This class is effectively a public API for unbundled + * applications, and must remain API stable. See README.txt in the root + * of this package for more information. + */ +public final class ProviderRequestUnbundled { + public static long INTERVAL_DISABLED; + public ProviderRequestUnbundled(ProviderRequest request) { + } + + /** + * True if this is an active request with a valid location reporting interval, false if this + * request is inactive and does not require any locations to be reported. + */ + public boolean getReportLocation() { + return false; + } + + /** + * The interval at which a provider should report location. Will return + * {@link #INTERVAL_DISABLED} for an inactive request. + */ + public long getInterval() { + return 0; + } + + /** + * The quality hint for this location request. The quality hint informs the provider how it + * should attempt to manage any accuracy vs power tradeoffs while attempting to satisfy this + * provider request. + */ + @RequiresApi(Build.VERSION_CODES.S) + public int getQuality() { + throw new UnsupportedOperationException(); + } + + /** + * The maximum time any location update may be delayed, and thus grouped with following updates + * to enable location batching. If the maximum update delay is equal to or greater than + * twice the interval, then the provider may provide batched results if possible. The maximum + * batch size a provider is allowed to return is the maximum update delay divided by the + * interval. + */ + @RequiresApi(Build.VERSION_CODES.S) + public long getMaxUpdateDelayMillis() { + throw new UnsupportedOperationException(); + } + + /** + * Whether any applicable hardware low power modes should be used to satisfy this request. + */ + @RequiresApi(Build.VERSION_CODES.S) + public boolean isLowPower() { + throw new UnsupportedOperationException(); + } + + /** + * Whether the provider should ignore all location settings, user consents, power restrictions + * or any other restricting factors and always satisfy this request to the best of their + * ability. This should only be used in case of a user initiated emergency. + */ + @RequiresApi(Build.VERSION_CODES.Q) + public boolean isLocationSettingsIgnored() { + throw new UnsupportedOperationException(); + } + + + /** + * The full list of location requests contributing to this provider request. + * + * @deprecated Do not use. + */ + @Deprecated + public @NonNull List getLocationRequests() { + throw new UnsupportedOperationException(); + } + + /** + * The power blame for this provider request. + */ + @RequiresApi(Build.VERSION_CODES.S) + public @NonNull WorkSource getWorkSource() { + throw new UnsupportedOperationException(); + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/location/WearableLocationService.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/location/WearableLocationService.java index 263f843636..fa472a351a 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/location/WearableLocationService.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/location/WearableLocationService.java @@ -104,24 +104,25 @@ public static Collection readLocationRequestList(DataMa } private static LocationRequestInternal readLocationRequest(DataMap dataMap, Context context) { - LocationRequestInternal request = new LocationRequestInternal(); + LocationRequest locationRequest = new LocationRequest(); + LocationRequestInternal request = new LocationRequestInternal(locationRequest); request.triggerUpdate = true; - request.request = new LocationRequest(); request.clients = Collections.emptyList(); if (dataMap.containsKey("PRIORITY")) - request.request.setPriority(dataMap.getInt("PRIORITY", 0)); + locationRequest.setPriority(dataMap.getInt("PRIORITY", 0)); if (dataMap.containsKey("INTERVAL_MS")) - request.request.setInterval(dataMap.getLong("INTERVAL_MS", 0)); + locationRequest.setInterval(dataMap.getLong("INTERVAL_MS", 0)); if (dataMap.containsKey("FASTEST_INTERVAL_MS")) - request.request.setFastestInterval(dataMap.getLong("FASTEST_INTERVAL_MS", 0)); - //if (dataMap.containsKey("MAX_WAIT_TIME_MS")) + locationRequest.setFastestInterval(dataMap.getLong("FASTEST_INTERVAL_MS", 0)); + if (dataMap.containsKey("MAX_WAIT_TIME_MS")) + locationRequest.setMaxWaitTime(dataMap.getLong("MAX_WAIT_TIME_MS", 0)); if (dataMap.containsKey("SMALLEST_DISPLACEMENT_METERS")) - request.request.setSmallestDisplacement(dataMap.getFloat("SMALLEST_DISPLACEMENT_METERS", 0)); + locationRequest.setSmallestDisplacement(dataMap.getFloat("SMALLEST_DISPLACEMENT_METERS", 0)); if (dataMap.containsKey("NUM_UPDATES")) - request.request.setNumUpdates(dataMap.getInt("NUM_UPDATES", 0)); + locationRequest.setNumUpdates(dataMap.getInt("NUM_UPDATES", 0)); if (dataMap.containsKey("EXPIRATION_DURATION_MS")) - request.request.setExpirationDuration(dataMap.getLong("EXPIRATION_DURATION_MS", 0)); + locationRequest.setExpirationDuration(dataMap.getLong("EXPIRATION_DURATION_MS", 0)); if (dataMap.containsKey("TAG")) request.tag = dataMap.getString("TAG"); if (dataMap.containsKey("CLIENTS_PACKAGE_ARRAY")) { diff --git a/settings.gradle b/settings.gradle index a5c6da78a9..4ca7a91f6f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -59,6 +59,7 @@ sublude ':play-services-droidguard:core' sublude ':play-services-fido:core' sublude ':play-services-gmscompliance:core' sublude ':play-services-location:core' +sublude ':play-services-location:system-api' include ':play-services-maps-core-mapbox' include ':play-services-maps-core-vtm' include ':play-services-maps-core-vtm:vtm-microg-theme'