From 7a84466008c0753fcda74656dd330d6af6def898 Mon Sep 17 00:00:00 2001 From: areteruhiro Date: Sat, 18 Jul 2026 11:31:46 +0900 Subject: [PATCH] feat: Add Maps support --- .github/workflows/crowdin_pull.yml | 2 +- .github/workflows/release.yml | 4 +- build.gradle | 5 +- .../microg/gms/settings/SettingsContract.kt | 36 + .../microg/gms/settings/SettingsProvider.kt | 59 + .../microg/gms/utils/SingleInstanceUtil.kt | 43 + .../main/res/drawable/ic_radio_checked.xml | 16 + .../main/res/drawable/ic_radio_unchecked.xml | 16 + .../core/src/main/res/values/themes.xml | 37 +- play-services-core/build.gradle | 14 +- .../src/main/res/values/themes.xml | 5 +- play-services-location/build.gradle | 43 + play-services-location/core/base/build.gradle | 51 + .../core/base/src/main/AndroidManifest.xml | 6 + .../microg/gms/location/LocationSettings.kt | 110 + .../org/microg/gms/location/extensions.kt | 89 + .../gms/location/network/OnlineSource.kt | 98 + play-services-location/core/build.gradle | 77 + .../core/provider/build.gradle | 58 + .../provider/src/main/AndroidManifest.xml | 88 + .../network/DatabaseExportFileProvider.kt | 23 + .../gms/location/network/LocationDatabase.kt | 674 + .../gms/location/network/NetworkDetails.kt | 11 + .../network/NetworkLocationRequest.kt | 34 + .../network/NetworkLocationService.kt | 621 + .../gms/location/network/cell/CellDetails.kt | 31 + .../network/cell/CellDetailsCallback.kt | 10 + .../network/cell/CellDetailsSource.kt | 74 + .../gms/location/network/cell/extensions.kt | 159 + .../microg/gms/location/network/extensions.kt | 39 + .../network/ichnaea/BluetoothBeacon.kt | 25 + .../gms/location/network/ichnaea/CellTower.kt | 45 + .../gms/location/network/ichnaea/Fallback.kt | 20 + .../network/ichnaea/GeolocateRequest.kt | 36 + .../network/ichnaea/GeolocateResponse.kt | 17 + .../location/network/ichnaea/GeosubmitItem.kt | 20 + .../network/ichnaea/GeosubmitPosition.kt | 49 + .../network/ichnaea/GeosubmitRequest.kt | 10 + .../network/ichnaea/GeosubmitSource.kt | 14 + .../network/ichnaea/IchnaeaServiceClient.kt | 246 + .../gms/location/network/ichnaea/RadioType.kt | 14 + .../network/ichnaea/RawGeolocateEntry.kt | 20 + .../location/network/ichnaea/ResponseError.kt | 11 + .../network/ichnaea/ResponseLocation.kt | 16 + .../network/ichnaea/ServiceException.kt | 8 + .../network/ichnaea/WifiAccessPoint.kt | 41 + .../location/network/ichnaea/extensions.kt | 260 + .../location/network/wifi/MovingWifiHelper.kt | 587 + .../gms/location/network/wifi/WifiDetails.kt | 18 + .../network/wifi/WifiDetailsCallback.kt | 11 + .../network/wifi/WifiDetailsSource.kt | 22 + .../network/wifi/WifiManagerSource.kt | 41 + .../network/wifi/WifiScannerSource.kt | 74 + .../gms/location/network/wifi/extensions.kt | 100 + .../AbstractLocationProviderPreTiramisu.kt | 70 + .../provider/FusedLocationProviderService.kt | 78 + .../provider/GenericLocationProvider.kt | 18 + .../provider/GeocodeProviderService.kt | 29 + .../IntentLocationProviderPreTiramisu.kt | 124 + .../provider/IntentLocationProviderService.kt | 91 + .../NetworkLocationProviderService.kt | 74 + .../OpenStreetMapNominatimGeocodeProvider.kt | 188 + .../gms/location/provider/extensions.kt | 28 + .../main/res/xml/location_exported_files.xml | 7 + .../core/src/huawei/AndroidManifest.xml | 34 + .../AskPermissionNotificationActivity.kt | 270 + .../res/drawable-zh/permission_step_1.jpg | Bin 0 -> 38513 bytes .../res/drawable-zh/permission_step_2.jpg | Bin 0 -> 55318 bytes .../core/src/huawei/res/drawable/arrow.png | Bin 0 -> 297 bytes .../core/src/huawei/res/drawable/ic_app.png | Bin 0 -> 8784 bytes .../drawable/ic_permission_notification.xml | 5 + .../huawei/res/drawable/permission_step_1.jpg | Bin 0 -> 41041 bytes .../huawei/res/drawable/permission_step_2.jpg | Bin 0 -> 57346 bytes .../layout/extended_permission_request.xml | 113 + .../src/huawei/res/values-zh-rCN/strings.xml | 23 + .../core/src/huawei/res/values/strings.xml | 27 + .../core/src/main/AndroidManifest.xml | 88 + .../LocationHistorySettingsActivity.kt | 26 + .../AbstractLocationManagerInstance.kt | 268 + .../location/manager/AskPermissionActivity.kt | 147 + .../manager/DeviceOrientationManager.kt | 345 + .../location/manager/LastLocationCapsule.kt | 162 + .../location/manager/LocationAppsDatabase.kt | 131 + .../gms/location/manager/LocationManager.kt | 394 + .../manager/LocationManagerInstance.kt | 420 + .../manager/LocationManagerService.kt | 66 + .../location/manager/LocationPostProcessor.kt | 147 + .../manager/LocationRequestManager.kt | 491 + .../microg/gms/location/manager/extensions.kt | 234 + .../reporting/ReportingAndroidService.kt | 28 + .../reporting/ReportingServiceInstance.kt | 70 + .../gms/location/reporting/extensions.kt | 8 + .../DetailedLocationSettingsStates.kt | 72 + .../GoogleLocationSettingsActivity.kt | 19 + .../LocationSettingsCheckerActivity.kt | 229 + .../ui/ConfigurationRequiredReceiver.kt | 49 + .../location/ui/LocationAllAppsFragment.kt | 86 + .../gms/location/ui/LocationAppFragment.kt | 132 + .../gms/location/ui/LocationMapPreference.kt | 110 + .../ui/LocationPreferencesFragment.kt | 402 + .../org/microg/gms/location/ui/extensions.kt | 8 + .../core/src/main/res/drawable/ic_gps.xml | 16 + .../src/main/res/drawable/ic_location.xml | 16 + .../res/layout/location_settings_dialog.xml | 68 + .../layout/location_settings_dialog_item.xml | 25 + .../res/layout/preference_full_container.xml | 12 + .../layout/preference_location_custom_url.xml | 45 + .../preference_location_online_source.xml | 84 + .../src/main/res/navigation/nav_location.xml | 44 + .../core/src/main/res/values-ar/strings.xml | 48 + .../core/src/main/res/values-ast/strings.xml | 18 + .../core/src/main/res/values-az/strings.xml | 32 + .../core/src/main/res/values-be/strings.xml | 47 + .../core/src/main/res/values-bn/strings.xml | 4 + .../core/src/main/res/values-ca/strings.xml | 48 + .../core/src/main/res/values-cs/strings.xml | 53 + .../core/src/main/res/values-de/strings.xml | 48 + .../core/src/main/res/values-eo/strings.xml | 2 + .../core/src/main/res/values-es/strings.xml | 48 + .../core/src/main/res/values-fa/strings.xml | 48 + .../core/src/main/res/values-fi/strings.xml | 13 + .../core/src/main/res/values-fil/strings.xml | 48 + .../core/src/main/res/values-fr/strings.xml | 48 + .../core/src/main/res/values-ga/strings.xml | 53 + .../core/src/main/res/values-hi/strings.xml | 3 + .../core/src/main/res/values-hu/strings.xml | 14 + .../core/src/main/res/values-in/strings.xml | 48 + .../core/src/main/res/values-is/strings.xml | 48 + .../core/src/main/res/values-it/strings.xml | 48 + .../core/src/main/res/values-iw/strings.xml | 5 + .../core/src/main/res/values-ja/strings.xml | 48 + .../core/src/main/res/values-ko/strings.xml | 48 + .../core/src/main/res/values-lt/strings.xml | 3 + .../core/src/main/res/values-lv/strings.xml | 6 + .../core/src/main/res/values-lzh/strings.xml | 2 + .../core/src/main/res/values-ml/strings.xml | 12 + .../core/src/main/res/values-mn/strings.xml | 4 + .../src/main/res/values-nb-rNO/strings.xml | 48 + .../core/src/main/res/values-nl/strings.xml | 48 + .../core/src/main/res/values-pl/strings.xml | 53 + .../src/main/res/values-pt-rBR/strings.xml | 53 + .../core/src/main/res/values-pt/strings.xml | 48 + .../core/src/main/res/values-ro/strings.xml | 53 + .../core/src/main/res/values-ru/strings.xml | 53 + .../core/src/main/res/values-si/strings.xml | 3 + .../core/src/main/res/values-sl/strings.xml | 3 + .../core/src/main/res/values-sr/strings.xml | 48 + .../core/src/main/res/values-sv/strings.xml | 48 + .../core/src/main/res/values-ta/strings.xml | 48 + .../core/src/main/res/values-th/strings.xml | 53 + .../core/src/main/res/values-tr/strings.xml | 48 + .../core/src/main/res/values-ug/strings.xml | 48 + .../core/src/main/res/values-uk/strings.xml | 53 + .../core/src/main/res/values-vi/strings.xml | 48 + .../src/main/res/values-zh-rCN/strings.xml | 53 + .../src/main/res/values-zh-rTW/strings.xml | 48 + .../core/src/main/res/values/strings.xml | 65 + .../src/main/res/xml/preferences_location.xml | 101 + .../res/xml/preferences_location_all_apps.xml | 19 + .../xml/preferences_location_app_details.xml | 37 + .../core/system-api/build.gradle | 23 + .../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 | 473 + .../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 + .../src/main/AndroidManifest.xml | 18 + .../location/ActivityRecognitionRequest.aidl | 3 + .../location/ActivityRecognitionResult.aidl | 3 + .../location/ActivityTransitionRequest.aidl | 3 + .../gms/location/CurrentLocationRequest.aidl | 3 + .../gms/location/DeviceOrientation.aidl | 3 + .../gms/location/GeofencingRequest.aidl | 3 + .../android/gms/location/GestureRequest.aidl | 3 + .../location/IDeviceOrientationListener.aidl | 7 + .../gms/location/ILocationCallback.aidl | 12 + .../gms/location/ILocationListener.aidl | 8 + .../gms/location/LastLocationRequest.aidl | 3 + .../gms/location/LocationAvailability.aidl | 3 + .../location/LocationAvailabilityRequest.aidl | 3 + .../android/gms/location/LocationRequest.aidl | 3 + .../android/gms/location/LocationResult.aidl | 3 + .../gms/location/LocationSettingsRequest.aidl | 3 + .../gms/location/LocationSettingsResult.aidl | 3 + .../android/gms/location/LocationStatus.aidl | 3 + .../gms/location/SleepSegmentRequest.aidl | 3 + .../DeviceOrientationRequestUpdateData.aidl | 3 + .../internal/FusedLocationProviderResult.aidl | 3 + .../internal/IBooleanStatusCallback.aidl | 7 + .../IFusedLocationProviderCallback.aidl | 8 + .../internal/IGeofencerCallbacks.aidl | 9 + .../IGoogleLocationManagerService.aidl | 158 + .../ILocationAvailabilityStatusCallback.aidl | 8 + .../internal/ILocationStatusCallback.aidl | 8 + .../location/internal/ISettingsCallbacks.aidl | 7 + .../location/internal/LocationReceiver.aidl | 3 + .../internal/LocationRequestInternal.aidl | 3 + .../internal/LocationRequestUpdateData.aidl | 3 + .../location/internal/ParcelableGeofence.aidl | 3 + .../internal/RemoveGeofencingRequest.aidl | 3 + .../SetGoogleLocationAccuracyRequest.aidl | 8 + .../gms/location/reporting/OptInRequest.aidl | 3 + .../location/reporting/ReportingState.aidl | 3 + .../location/reporting/SendDataRequest.aidl | 3 + .../reporting/UlrPrivateModeRequest.aidl | 3 + .../gms/location/reporting/UploadRequest.aidl | 3 + .../reporting/UploadRequestResult.aidl | 3 + .../reporting/internal/IReportingService.aidl | 21 + .../gms/location/ActivityRecognition.java | 57 + .../gms/location/ActivityRecognitionApi.java | 109 + .../location/ActivityRecognitionClient.java | 107 + .../location/ActivityRecognitionRequest.java | 40 + .../location/ActivityRecognitionResult.java | 173 + .../gms/location/ActivityTransition.java | 122 + .../gms/location/ActivityTransitionEvent.java | 91 + .../location/ActivityTransitionRequest.java | 107 + .../location/ActivityTransitionResult.java | 91 + .../gms/location/CurrentLocationRequest.java | 269 + .../gms/location/DetectedActivity.java | 147 + .../gms/location/DeviceOrientation.java | 136 + .../location/DeviceOrientationRequest.java | 45 + .../location/FusedLocationProviderApi.java | 268 + .../location/FusedLocationProviderClient.java | 296 + .../google/android/gms/location/Geofence.java | 229 + .../gms/location/GeofenceStatusCodes.java | 64 + .../android/gms/location/GeofencingApi.java | 58 + .../gms/location/GeofencingClient.java | 41 + .../android/gms/location/GeofencingEvent.java | 133 + .../gms/location/GeofencingRequest.java | 163 + .../android/gms/location/GestureRequest.java | 25 + .../android/gms/location/Granularity.java | 43 + .../gms/location/LastLocationRequest.java | 163 + .../gms/location/LocationAvailability.java | 134 + .../location/LocationAvailabilityRequest.java | 21 + .../gms/location/LocationCallback.java | 44 + .../android/gms/location/LocationClient.java | 105 + .../gms/location/LocationListener.java | 35 + .../android/gms/location/LocationRequest.java | 853 + .../android/gms/location/LocationResult.java | 121 + .../gms/location/LocationServices.java | 111 + .../LocationSettingsConfiguration.java | 21 + .../gms/location/LocationSettingsRequest.java | 135 + .../location/LocationSettingsResponse.java | 41 + .../gms/location/LocationSettingsResult.java | 86 + .../gms/location/LocationSettingsStates.java | 138 + .../location/LocationSettingsStatusCodes.java | 43 + .../android/gms/location/LocationStatus.java | 94 + .../gms/location/LocationStatusCodes.java | 41 + .../gms/location/NetworkLocationStatus.java | 33 + .../google/android/gms/location/Priority.java | 49 + .../android/gms/location/SettingsApi.java | 36 + .../android/gms/location/SettingsClient.java | 44 + .../gms/location/SleepClassifyEvent.java | 77 + .../gms/location/SleepSegmentEvent.java | 36 + .../gms/location/SleepSegmentRequest.java | 72 + .../gms/location/ThrottleBehavior.java | 24 + .../gms/location/internal/ClientIdentity.java | 76 + .../DeviceOrientationRequestInternal.java | 38 + .../DeviceOrientationRequestUpdateData.java | 41 + .../internal/FusedLocationProviderResult.java | 42 + .../location/internal/LocationReceiver.java | 112 + .../internal/LocationRequestInternal.java | 132 + .../internal/LocationRequestUpdateData.java | 60 + .../location/internal/ParcelableGeofence.java | 104 + .../internal/RemoveGeofencingRequest.java | 50 + .../SetGoogleLocationAccuracyRequest.java | 53 + .../gms/location/reporting/OptInRequest.java | 21 + .../location/reporting/ReportingState.java | 82 + .../location/reporting/SendDataRequest.java | 17 + .../reporting/UlrPrivateModeRequest.java | 17 + .../gms/location/reporting/UploadRequest.java | 41 + .../reporting/UploadRequestResult.java | 34 + .../ActivityRecognitionApiClientBuilder.java | 35 + .../location/ActivityRecognitionApiImpl.java | 67 + .../ActivityRecognitionClientImpl.java | 38 + .../FusedLocationProviderApiImpl.java | 177 + .../FusedLocationProviderClientImpl.java | 128 + .../gms/location/GeofencingApiImpl.java | 116 + .../gms/location/GeofencingClientImpl.java | 43 + .../location/GoogleLocationManagerClient.java | 52 + .../microg/gms/location/GranularityUtil.java | 42 + .../gms/location/LocationClientImpl.java | 180 + .../LocationServicesApiClientBuilder.java | 35 + .../org/microg/gms/location/PriorityUtil.java | 46 + .../microg/gms/location/SettingsApiImpl.java | 33 + .../gms/location/SettingsClientImpl.java | 39 + .../gms/location/ThrottleBehaviorUtil.java | 43 + play-services-maps/build.gradle | 43 + play-services-maps/core/hms/build.gradle | 61 + .../core/hms/proguard-rules.pro | 9 + .../core/hms/src/main/AndroidManifest.xml | 20 + .../core/hms/src/main/assets/.gitignore | 1 + .../gms/maps_dynamite/ModuleDescriptor.java | 14 + .../gms/maps/internal/CreatorImpl.java | 93 + .../gms/maps/hms/CameraUpdateFactory.kt | 69 + .../org/microg/gms/maps/hms/GoogleMap.kt | 991 ++ .../org/microg/gms/maps/hms/MapFragment.kt | 115 + .../kotlin/org/microg/gms/maps/hms/MapView.kt | 69 + .../org/microg/gms/maps/hms/Projection.kt | 146 + .../maps/hms/StreetViewPanoramaFragment.kt | 63 + .../gms/maps/hms/StreetViewPanoramaView.kt | 54 + .../org/microg/gms/maps/hms/UiSettings.kt | 332 + .../maps/hms/model/BitmapDescriptorFactory.kt | 66 + .../org/microg/gms/maps/hms/model/Circle.kt | 124 + .../gms/maps/hms/model/GroundOverlay.kt | 144 + .../org/microg/gms/maps/hms/model/Marker.kt | 154 + .../org/microg/gms/maps/hms/model/Polygon.kt | 172 + .../org/microg/gms/maps/hms/model/Polyline.kt | 220 + .../microg/gms/maps/hms/model/TileOverlay.kt | 81 + .../gms/maps/hms/utils/MapCompatAdapter.kt | 108 + .../microg/gms/maps/hms/utils/MapContext.kt | 152 + .../gms/maps/hms/utils/MapUiController.kt | 71 + .../gms/maps/hms/utils/typeConverter.kt | 239 + .../res/drawable-hdpi/maps_default_marker.png | Bin 0 -> 1974 bytes .../res/drawable-ldpi/maps_default_marker.png | Bin 0 -> 946 bytes .../res/drawable-mdpi/maps_default_marker.png | Bin 0 -> 1277 bytes .../drawable-xhdpi/maps_default_marker.png | Bin 0 -> 2745 bytes .../drawable-xxhdpi/maps_default_marker.png | Bin 0 -> 4277 bytes .../drawable-xxxhdpi/maps_default_marker.png | Bin 0 -> 5890 bytes .../main/res/raw/mapstyle_grayscale_hms.json | 101 + .../src/main/res/raw/mapstyle_night_hms.json | 376 + .../core/hms/src/main/res/values/strings.xml | 1 + play-services-maps/core/mapbox/build.gradle | 84 + .../core/mapbox/src/main/AndroidManifest.xml | 35 + ...Open Sans Regular,Arial Unicode MS Regular | 1 + .../src/main/assets/Roboto Bold/0-255.pbf | Bin 0 -> 84683 bytes .../src/main/assets/Roboto Bold/1024-1279.pbf | Bin 0 -> 137401 bytes .../main/assets/Roboto Bold/10240-10495.pbf | 3 + .../main/assets/Roboto Bold/10496-10751.pbf | 3 + .../main/assets/Roboto Bold/10752-11007.pbf | 3 + .../main/assets/Roboto Bold/11008-11263.pbf | 3 + .../main/assets/Roboto Bold/11264-11519.pbf | 3 + .../main/assets/Roboto Bold/11520-11775.pbf | 3 + .../main/assets/Roboto Bold/11776-12031.pbf | 3 + .../main/assets/Roboto Bold/12032-12287.pbf | 3 + .../main/assets/Roboto Bold/12288-12543.pbf | 3 + .../main/assets/Roboto Bold/12544-12799.pbf | 3 + .../src/main/assets/Roboto Bold/1280-1535.pbf | Bin 0 -> 11193 bytes .../main/assets/Roboto Bold/12800-13055.pbf | 3 + .../main/assets/Roboto Bold/13056-13311.pbf | 3 + .../main/assets/Roboto Bold/13312-13567.pbf | 3 + .../main/assets/Roboto Bold/13568-13823.pbf | 3 + .../main/assets/Roboto Bold/13824-14079.pbf | 3 + .../main/assets/Roboto Bold/14080-14335.pbf | 3 + .../main/assets/Roboto Bold/14336-14591.pbf | 3 + .../main/assets/Roboto Bold/14592-14847.pbf | 3 + .../main/assets/Roboto Bold/14848-15103.pbf | 3 + .../main/assets/Roboto Bold/15104-15359.pbf | 3 + .../src/main/assets/Roboto Bold/1536-1791.pbf | 3 + .../main/assets/Roboto Bold/15360-15615.pbf | 3 + .../main/assets/Roboto Bold/15616-15871.pbf | 3 + .../main/assets/Roboto Bold/15872-16127.pbf | 3 + .../main/assets/Roboto Bold/16128-16383.pbf | 3 + .../main/assets/Roboto Bold/16384-16639.pbf | 3 + .../main/assets/Roboto Bold/16640-16895.pbf | 3 + .../main/assets/Roboto Bold/16896-17151.pbf | 3 + .../main/assets/Roboto Bold/17152-17407.pbf | 3 + .../main/assets/Roboto Bold/17408-17663.pbf | 3 + .../main/assets/Roboto Bold/17664-17919.pbf | 3 + .../src/main/assets/Roboto Bold/1792-2047.pbf | 3 + .../main/assets/Roboto Bold/17920-18175.pbf | 3 + .../main/assets/Roboto Bold/18176-18431.pbf | 3 + .../main/assets/Roboto Bold/18432-18687.pbf | 3 + .../main/assets/Roboto Bold/18688-18943.pbf | 3 + .../main/assets/Roboto Bold/18944-19199.pbf | 3 + .../main/assets/Roboto Bold/19200-19455.pbf | 3 + .../main/assets/Roboto Bold/19456-19711.pbf | 3 + .../main/assets/Roboto Bold/19712-19967.pbf | 3 + .../main/assets/Roboto Bold/19968-20223.pbf | 3 + .../main/assets/Roboto Bold/20224-20479.pbf | 3 + .../src/main/assets/Roboto Bold/2048-2303.pbf | 3 + .../main/assets/Roboto Bold/20480-20735.pbf | 3 + .../main/assets/Roboto Bold/20736-20991.pbf | 3 + .../main/assets/Roboto Bold/20992-21247.pbf | 3 + .../main/assets/Roboto Bold/21248-21503.pbf | 3 + .../main/assets/Roboto Bold/21504-21759.pbf | 3 + .../main/assets/Roboto Bold/21760-22015.pbf | 3 + .../main/assets/Roboto Bold/22016-22271.pbf | 3 + .../main/assets/Roboto Bold/22272-22527.pbf | 3 + .../main/assets/Roboto Bold/22528-22783.pbf | 3 + .../main/assets/Roboto Bold/22784-23039.pbf | 3 + .../src/main/assets/Roboto Bold/2304-2559.pbf | 3 + .../main/assets/Roboto Bold/23040-23295.pbf | 3 + .../main/assets/Roboto Bold/23296-23551.pbf | 3 + .../main/assets/Roboto Bold/23552-23807.pbf | 3 + .../main/assets/Roboto Bold/23808-24063.pbf | 3 + .../main/assets/Roboto Bold/24064-24319.pbf | 3 + .../main/assets/Roboto Bold/24320-24575.pbf | 3 + .../main/assets/Roboto Bold/24576-24831.pbf | 3 + .../main/assets/Roboto Bold/24832-25087.pbf | 3 + .../main/assets/Roboto Bold/25088-25343.pbf | 3 + .../main/assets/Roboto Bold/25344-25599.pbf | 3 + .../src/main/assets/Roboto Bold/256-511.pbf | Bin 0 -> 74748 bytes .../src/main/assets/Roboto Bold/2560-2815.pbf | 3 + .../main/assets/Roboto Bold/25600-25855.pbf | 3 + .../main/assets/Roboto Bold/25856-26111.pbf | 3 + .../main/assets/Roboto Bold/26112-26367.pbf | 3 + .../main/assets/Roboto Bold/26368-26623.pbf | 3 + .../main/assets/Roboto Bold/26624-26879.pbf | 3 + .../main/assets/Roboto Bold/26880-27135.pbf | 3 + .../main/assets/Roboto Bold/27136-27391.pbf | 3 + .../main/assets/Roboto Bold/27392-27647.pbf | 3 + .../main/assets/Roboto Bold/27648-27903.pbf | 3 + .../main/assets/Roboto Bold/27904-28159.pbf | 3 + .../src/main/assets/Roboto Bold/2816-3071.pbf | 3 + .../main/assets/Roboto Bold/28160-28415.pbf | 3 + .../main/assets/Roboto Bold/28416-28671.pbf | 3 + .../main/assets/Roboto Bold/28672-28927.pbf | 3 + .../main/assets/Roboto Bold/28928-29183.pbf | 3 + .../main/assets/Roboto Bold/29184-29439.pbf | 3 + .../main/assets/Roboto Bold/29440-29695.pbf | 3 + .../main/assets/Roboto Bold/29696-29951.pbf | 3 + .../main/assets/Roboto Bold/29952-30207.pbf | 3 + .../main/assets/Roboto Bold/30208-30463.pbf | 3 + .../main/assets/Roboto Bold/30464-30719.pbf | 3 + .../src/main/assets/Roboto Bold/3072-3327.pbf | 3 + .../main/assets/Roboto Bold/30720-30975.pbf | 3 + .../main/assets/Roboto Bold/30976-31231.pbf | 3 + .../main/assets/Roboto Bold/31232-31487.pbf | 3 + .../main/assets/Roboto Bold/31488-31743.pbf | 3 + .../main/assets/Roboto Bold/31744-31999.pbf | 3 + .../main/assets/Roboto Bold/32000-32255.pbf | 3 + .../main/assets/Roboto Bold/32256-32511.pbf | 3 + .../main/assets/Roboto Bold/32512-32767.pbf | 3 + .../main/assets/Roboto Bold/32768-33023.pbf | 3 + .../main/assets/Roboto Bold/33024-33279.pbf | 3 + .../src/main/assets/Roboto Bold/3328-3583.pbf | 3 + .../main/assets/Roboto Bold/33280-33535.pbf | 3 + .../main/assets/Roboto Bold/33536-33791.pbf | 3 + .../main/assets/Roboto Bold/33792-34047.pbf | 3 + .../main/assets/Roboto Bold/34048-34303.pbf | 3 + .../main/assets/Roboto Bold/34304-34559.pbf | 3 + .../main/assets/Roboto Bold/34560-34815.pbf | 3 + .../main/assets/Roboto Bold/34816-35071.pbf | 3 + .../main/assets/Roboto Bold/35072-35327.pbf | 3 + .../main/assets/Roboto Bold/35328-35583.pbf | 3 + .../main/assets/Roboto Bold/35584-35839.pbf | 3 + .../src/main/assets/Roboto Bold/3584-3839.pbf | 3 + .../main/assets/Roboto Bold/35840-36095.pbf | 3 + .../main/assets/Roboto Bold/36096-36351.pbf | 3 + .../main/assets/Roboto Bold/36352-36607.pbf | 3 + .../main/assets/Roboto Bold/36608-36863.pbf | 3 + .../main/assets/Roboto Bold/36864-37119.pbf | 3 + .../main/assets/Roboto Bold/37120-37375.pbf | 3 + .../main/assets/Roboto Bold/37376-37631.pbf | 3 + .../main/assets/Roboto Bold/37632-37887.pbf | 3 + .../main/assets/Roboto Bold/37888-38143.pbf | 3 + .../main/assets/Roboto Bold/38144-38399.pbf | 3 + .../src/main/assets/Roboto Bold/3840-4095.pbf | 3 + .../main/assets/Roboto Bold/38400-38655.pbf | 3 + .../main/assets/Roboto Bold/38656-38911.pbf | 3 + .../main/assets/Roboto Bold/38912-39167.pbf | 3 + .../main/assets/Roboto Bold/39168-39423.pbf | 3 + .../main/assets/Roboto Bold/39424-39679.pbf | 3 + .../main/assets/Roboto Bold/39680-39935.pbf | 3 + .../main/assets/Roboto Bold/39936-40191.pbf | 3 + .../main/assets/Roboto Bold/40192-40447.pbf | 3 + .../main/assets/Roboto Bold/40448-40703.pbf | 3 + .../main/assets/Roboto Bold/40704-40959.pbf | 3 + .../src/main/assets/Roboto Bold/4096-4351.pbf | 3 + .../main/assets/Roboto Bold/40960-41215.pbf | 3 + .../main/assets/Roboto Bold/41216-41471.pbf | 3 + .../main/assets/Roboto Bold/41472-41727.pbf | 3 + .../main/assets/Roboto Bold/41728-41983.pbf | 3 + .../main/assets/Roboto Bold/41984-42239.pbf | 3 + .../main/assets/Roboto Bold/42240-42495.pbf | 3 + .../main/assets/Roboto Bold/42496-42751.pbf | 3 + .../main/assets/Roboto Bold/42752-43007.pbf | 3 + .../main/assets/Roboto Bold/43008-43263.pbf | 3 + .../main/assets/Roboto Bold/43264-43519.pbf | 3 + .../src/main/assets/Roboto Bold/4352-4607.pbf | 3 + .../main/assets/Roboto Bold/43520-43775.pbf | 3 + .../main/assets/Roboto Bold/43776-44031.pbf | 3 + .../main/assets/Roboto Bold/44032-44287.pbf | 3 + .../main/assets/Roboto Bold/44288-44543.pbf | 3 + .../main/assets/Roboto Bold/44544-44799.pbf | 3 + .../main/assets/Roboto Bold/44800-45055.pbf | 3 + .../main/assets/Roboto Bold/45056-45311.pbf | 3 + .../main/assets/Roboto Bold/45312-45567.pbf | 3 + .../main/assets/Roboto Bold/45568-45823.pbf | 3 + .../main/assets/Roboto Bold/45824-46079.pbf | 3 + .../src/main/assets/Roboto Bold/4608-4863.pbf | 3 + .../main/assets/Roboto Bold/46080-46335.pbf | 3 + .../main/assets/Roboto Bold/46336-46591.pbf | 3 + .../main/assets/Roboto Bold/46592-46847.pbf | 3 + .../main/assets/Roboto Bold/46848-47103.pbf | 3 + .../main/assets/Roboto Bold/47104-47359.pbf | 3 + .../main/assets/Roboto Bold/47360-47615.pbf | 3 + .../main/assets/Roboto Bold/47616-47871.pbf | 3 + .../main/assets/Roboto Bold/47872-48127.pbf | 3 + .../main/assets/Roboto Bold/48128-48383.pbf | 3 + .../main/assets/Roboto Bold/48384-48639.pbf | 3 + .../src/main/assets/Roboto Bold/4864-5119.pbf | 3 + .../main/assets/Roboto Bold/48640-48895.pbf | 3 + .../main/assets/Roboto Bold/48896-49151.pbf | 3 + .../main/assets/Roboto Bold/49152-49407.pbf | 3 + .../main/assets/Roboto Bold/49408-49663.pbf | 3 + .../main/assets/Roboto Bold/49664-49919.pbf | 3 + .../main/assets/Roboto Bold/49920-50175.pbf | 3 + .../main/assets/Roboto Bold/50176-50431.pbf | 3 + .../main/assets/Roboto Bold/50432-50687.pbf | 3 + .../main/assets/Roboto Bold/50688-50943.pbf | 3 + .../main/assets/Roboto Bold/50944-51199.pbf | 3 + .../src/main/assets/Roboto Bold/512-767.pbf | Bin 0 -> 4774 bytes .../src/main/assets/Roboto Bold/5120-5375.pbf | 3 + .../main/assets/Roboto Bold/51200-51455.pbf | 3 + .../main/assets/Roboto Bold/51456-51711.pbf | 3 + .../main/assets/Roboto Bold/51712-51967.pbf | 3 + .../main/assets/Roboto Bold/51968-52223.pbf | 3 + .../main/assets/Roboto Bold/52224-52479.pbf | 3 + .../main/assets/Roboto Bold/52480-52735.pbf | 3 + .../main/assets/Roboto Bold/52736-52991.pbf | 3 + .../main/assets/Roboto Bold/52992-53247.pbf | 3 + .../main/assets/Roboto Bold/53248-53503.pbf | 3 + .../main/assets/Roboto Bold/53504-53759.pbf | 3 + .../src/main/assets/Roboto Bold/5376-5631.pbf | 3 + .../main/assets/Roboto Bold/53760-54015.pbf | 3 + .../main/assets/Roboto Bold/54016-54271.pbf | 3 + .../main/assets/Roboto Bold/54272-54527.pbf | 3 + .../main/assets/Roboto Bold/54528-54783.pbf | 3 + .../main/assets/Roboto Bold/54784-55039.pbf | 3 + .../main/assets/Roboto Bold/55040-55295.pbf | 3 + .../main/assets/Roboto Bold/55296-55551.pbf | 3 + .../main/assets/Roboto Bold/55552-55807.pbf | 3 + .../main/assets/Roboto Bold/55808-56063.pbf | 3 + .../main/assets/Roboto Bold/56064-56319.pbf | 3 + .../src/main/assets/Roboto Bold/5632-5887.pbf | 3 + .../main/assets/Roboto Bold/56320-56575.pbf | 3 + .../main/assets/Roboto Bold/56576-56831.pbf | 3 + .../main/assets/Roboto Bold/56832-57087.pbf | 3 + .../main/assets/Roboto Bold/57088-57343.pbf | 3 + .../main/assets/Roboto Bold/57344-57599.pbf | 3 + .../main/assets/Roboto Bold/57600-57855.pbf | 3 + .../main/assets/Roboto Bold/57856-58111.pbf | 3 + .../main/assets/Roboto Bold/58112-58367.pbf | 3 + .../main/assets/Roboto Bold/58368-58623.pbf | 3 + .../main/assets/Roboto Bold/58624-58879.pbf | 3 + .../src/main/assets/Roboto Bold/5888-6143.pbf | 3 + .../main/assets/Roboto Bold/58880-59135.pbf | 3 + .../main/assets/Roboto Bold/59136-59391.pbf | 3 + .../main/assets/Roboto Bold/59392-59647.pbf | 3 + .../main/assets/Roboto Bold/59648-59903.pbf | 3 + .../main/assets/Roboto Bold/59904-60159.pbf | 3 + .../main/assets/Roboto Bold/60160-60415.pbf | 3 + .../main/assets/Roboto Bold/60416-60671.pbf | 3 + .../main/assets/Roboto Bold/60672-60927.pbf | 3 + .../main/assets/Roboto Bold/60928-61183.pbf | 3 + .../main/assets/Roboto Bold/61184-61439.pbf | 3 + .../src/main/assets/Roboto Bold/6144-6399.pbf | 3 + .../main/assets/Roboto Bold/61440-61695.pbf | 3 + .../main/assets/Roboto Bold/61696-61951.pbf | 3 + .../main/assets/Roboto Bold/61952-62207.pbf | 3 + .../main/assets/Roboto Bold/62208-62463.pbf | 3 + .../main/assets/Roboto Bold/62464-62719.pbf | 3 + .../main/assets/Roboto Bold/62720-62975.pbf | 3 + .../main/assets/Roboto Bold/62976-63231.pbf | Bin 0 -> 168 bytes .../main/assets/Roboto Bold/63232-63487.pbf | 3 + .../main/assets/Roboto Bold/63488-63743.pbf | 3 + .../main/assets/Roboto Bold/63744-63999.pbf | 3 + .../src/main/assets/Roboto Bold/6400-6655.pbf | 3 + .../main/assets/Roboto Bold/64000-64255.pbf | 3 + .../main/assets/Roboto Bold/64256-64511.pbf | Bin 0 -> 2459 bytes .../main/assets/Roboto Bold/64512-64767.pbf | 3 + .../main/assets/Roboto Bold/64768-65023.pbf | 3 + .../main/assets/Roboto Bold/65024-65279.pbf | Bin 0 -> 82 bytes .../main/assets/Roboto Bold/65280-65535.pbf | Bin 0 -> 2041 bytes .../src/main/assets/Roboto Bold/6656-6911.pbf | 3 + .../src/main/assets/Roboto Bold/6912-7167.pbf | 3 + .../src/main/assets/Roboto Bold/7168-7423.pbf | 3 + .../src/main/assets/Roboto Bold/7424-7679.pbf | 3 + .../src/main/assets/Roboto Bold/768-1023.pbf | Bin 0 -> 37736 bytes .../src/main/assets/Roboto Bold/7680-7935.pbf | Bin 0 -> 60415 bytes .../src/main/assets/Roboto Bold/7936-8191.pbf | Bin 0 -> 771 bytes .../src/main/assets/Roboto Bold/8192-8447.pbf | Bin 0 -> 15677 bytes .../src/main/assets/Roboto Bold/8448-8703.pbf | Bin 0 -> 5442 bytes .../src/main/assets/Roboto Bold/8704-8959.pbf | Bin 0 -> 5494 bytes .../src/main/assets/Roboto Bold/8960-9215.pbf | 3 + .../src/main/assets/Roboto Bold/9216-9471.pbf | 3 + .../src/main/assets/Roboto Bold/9472-9727.pbf | Bin 0 -> 478 bytes .../src/main/assets/Roboto Bold/9728-9983.pbf | 3 + .../main/assets/Roboto Bold/9984-10239.pbf | 4 + .../src/main/assets/Roboto Medium/0-255.pbf | Bin 0 -> 82965 bytes .../main/assets/Roboto Medium/1024-1279.pbf | Bin 0 -> 135083 bytes .../main/assets/Roboto Medium/10240-10495.pbf | 3 + .../main/assets/Roboto Medium/10496-10751.pbf | 3 + .../main/assets/Roboto Medium/10752-11007.pbf | 3 + .../main/assets/Roboto Medium/11008-11263.pbf | 3 + .../main/assets/Roboto Medium/11264-11519.pbf | 3 + .../main/assets/Roboto Medium/11520-11775.pbf | 3 + .../main/assets/Roboto Medium/11776-12031.pbf | 3 + .../main/assets/Roboto Medium/12032-12287.pbf | 3 + .../main/assets/Roboto Medium/12288-12543.pbf | 3 + .../main/assets/Roboto Medium/12544-12799.pbf | 3 + .../main/assets/Roboto Medium/1280-1535.pbf | Bin 0 -> 11032 bytes .../main/assets/Roboto Medium/12800-13055.pbf | 3 + .../main/assets/Roboto Medium/13056-13311.pbf | 3 + .../main/assets/Roboto Medium/13312-13567.pbf | 3 + .../main/assets/Roboto Medium/13568-13823.pbf | 3 + .../main/assets/Roboto Medium/13824-14079.pbf | 3 + .../main/assets/Roboto Medium/14080-14335.pbf | 3 + .../main/assets/Roboto Medium/14336-14591.pbf | 3 + .../main/assets/Roboto Medium/14592-14847.pbf | 3 + .../main/assets/Roboto Medium/14848-15103.pbf | 3 + .../main/assets/Roboto Medium/15104-15359.pbf | 3 + .../main/assets/Roboto Medium/1536-1791.pbf | 3 + .../main/assets/Roboto Medium/15360-15615.pbf | 3 + .../main/assets/Roboto Medium/15616-15871.pbf | 3 + .../main/assets/Roboto Medium/15872-16127.pbf | 3 + .../main/assets/Roboto Medium/16128-16383.pbf | 3 + .../main/assets/Roboto Medium/16384-16639.pbf | 3 + .../main/assets/Roboto Medium/16640-16895.pbf | 3 + .../main/assets/Roboto Medium/16896-17151.pbf | 3 + .../main/assets/Roboto Medium/17152-17407.pbf | 3 + .../main/assets/Roboto Medium/17408-17663.pbf | 3 + .../main/assets/Roboto Medium/17664-17919.pbf | 3 + .../main/assets/Roboto Medium/1792-2047.pbf | 3 + .../main/assets/Roboto Medium/17920-18175.pbf | 3 + .../main/assets/Roboto Medium/18176-18431.pbf | 3 + .../main/assets/Roboto Medium/18432-18687.pbf | 3 + .../main/assets/Roboto Medium/18688-18943.pbf | 3 + .../main/assets/Roboto Medium/18944-19199.pbf | 3 + .../main/assets/Roboto Medium/19200-19455.pbf | 3 + .../main/assets/Roboto Medium/19456-19711.pbf | 3 + .../main/assets/Roboto Medium/19712-19967.pbf | 3 + .../main/assets/Roboto Medium/19968-20223.pbf | 3 + .../main/assets/Roboto Medium/20224-20479.pbf | 3 + .../main/assets/Roboto Medium/2048-2303.pbf | 3 + .../main/assets/Roboto Medium/20480-20735.pbf | 3 + .../main/assets/Roboto Medium/20736-20991.pbf | 3 + .../main/assets/Roboto Medium/20992-21247.pbf | 3 + .../main/assets/Roboto Medium/21248-21503.pbf | 3 + .../main/assets/Roboto Medium/21504-21759.pbf | 3 + .../main/assets/Roboto Medium/21760-22015.pbf | 3 + .../main/assets/Roboto Medium/22016-22271.pbf | 3 + .../main/assets/Roboto Medium/22272-22527.pbf | 3 + .../main/assets/Roboto Medium/22528-22783.pbf | 3 + .../main/assets/Roboto Medium/22784-23039.pbf | 3 + .../main/assets/Roboto Medium/2304-2559.pbf | 3 + .../main/assets/Roboto Medium/23040-23295.pbf | 3 + .../main/assets/Roboto Medium/23296-23551.pbf | 3 + .../main/assets/Roboto Medium/23552-23807.pbf | 3 + .../main/assets/Roboto Medium/23808-24063.pbf | 3 + .../main/assets/Roboto Medium/24064-24319.pbf | 3 + .../main/assets/Roboto Medium/24320-24575.pbf | 3 + .../main/assets/Roboto Medium/24576-24831.pbf | 3 + .../main/assets/Roboto Medium/24832-25087.pbf | 3 + .../main/assets/Roboto Medium/25088-25343.pbf | 3 + .../main/assets/Roboto Medium/25344-25599.pbf | 3 + .../src/main/assets/Roboto Medium/256-511.pbf | Bin 0 -> 73791 bytes .../main/assets/Roboto Medium/2560-2815.pbf | 3 + .../main/assets/Roboto Medium/25600-25855.pbf | 3 + .../main/assets/Roboto Medium/25856-26111.pbf | 3 + .../main/assets/Roboto Medium/26112-26367.pbf | 3 + .../main/assets/Roboto Medium/26368-26623.pbf | 3 + .../main/assets/Roboto Medium/26624-26879.pbf | 3 + .../main/assets/Roboto Medium/26880-27135.pbf | 3 + .../main/assets/Roboto Medium/27136-27391.pbf | 3 + .../main/assets/Roboto Medium/27392-27647.pbf | 3 + .../main/assets/Roboto Medium/27648-27903.pbf | 3 + .../main/assets/Roboto Medium/27904-28159.pbf | 3 + .../main/assets/Roboto Medium/2816-3071.pbf | 3 + .../main/assets/Roboto Medium/28160-28415.pbf | 3 + .../main/assets/Roboto Medium/28416-28671.pbf | 3 + .../main/assets/Roboto Medium/28672-28927.pbf | 3 + .../main/assets/Roboto Medium/28928-29183.pbf | 3 + .../main/assets/Roboto Medium/29184-29439.pbf | 3 + .../main/assets/Roboto Medium/29440-29695.pbf | 3 + .../main/assets/Roboto Medium/29696-29951.pbf | 3 + .../main/assets/Roboto Medium/29952-30207.pbf | 3 + .../main/assets/Roboto Medium/30208-30463.pbf | 3 + .../main/assets/Roboto Medium/30464-30719.pbf | 3 + .../main/assets/Roboto Medium/3072-3327.pbf | 3 + .../main/assets/Roboto Medium/30720-30975.pbf | 3 + .../main/assets/Roboto Medium/30976-31231.pbf | 3 + .../main/assets/Roboto Medium/31232-31487.pbf | 3 + .../main/assets/Roboto Medium/31488-31743.pbf | 3 + .../main/assets/Roboto Medium/31744-31999.pbf | 3 + .../main/assets/Roboto Medium/32000-32255.pbf | 3 + .../main/assets/Roboto Medium/32256-32511.pbf | 3 + .../main/assets/Roboto Medium/32512-32767.pbf | 3 + .../main/assets/Roboto Medium/32768-33023.pbf | 3 + .../main/assets/Roboto Medium/33024-33279.pbf | 3 + .../main/assets/Roboto Medium/3328-3583.pbf | 3 + .../main/assets/Roboto Medium/33280-33535.pbf | 3 + .../main/assets/Roboto Medium/33536-33791.pbf | 3 + .../main/assets/Roboto Medium/33792-34047.pbf | 3 + .../main/assets/Roboto Medium/34048-34303.pbf | 3 + .../main/assets/Roboto Medium/34304-34559.pbf | 3 + .../main/assets/Roboto Medium/34560-34815.pbf | 3 + .../main/assets/Roboto Medium/34816-35071.pbf | 3 + .../main/assets/Roboto Medium/35072-35327.pbf | 3 + .../main/assets/Roboto Medium/35328-35583.pbf | 3 + .../main/assets/Roboto Medium/35584-35839.pbf | 3 + .../main/assets/Roboto Medium/3584-3839.pbf | 3 + .../main/assets/Roboto Medium/35840-36095.pbf | 3 + .../main/assets/Roboto Medium/36096-36351.pbf | 3 + .../main/assets/Roboto Medium/36352-36607.pbf | 3 + .../main/assets/Roboto Medium/36608-36863.pbf | 3 + .../main/assets/Roboto Medium/36864-37119.pbf | 3 + .../main/assets/Roboto Medium/37120-37375.pbf | 3 + .../main/assets/Roboto Medium/37376-37631.pbf | 3 + .../main/assets/Roboto Medium/37632-37887.pbf | 3 + .../main/assets/Roboto Medium/37888-38143.pbf | 3 + .../main/assets/Roboto Medium/38144-38399.pbf | 3 + .../main/assets/Roboto Medium/3840-4095.pbf | 3 + .../main/assets/Roboto Medium/38400-38655.pbf | 3 + .../main/assets/Roboto Medium/38656-38911.pbf | 3 + .../main/assets/Roboto Medium/38912-39167.pbf | 3 + .../main/assets/Roboto Medium/39168-39423.pbf | 3 + .../main/assets/Roboto Medium/39424-39679.pbf | 3 + .../main/assets/Roboto Medium/39680-39935.pbf | 3 + .../main/assets/Roboto Medium/39936-40191.pbf | 3 + .../main/assets/Roboto Medium/40192-40447.pbf | 3 + .../main/assets/Roboto Medium/40448-40703.pbf | 3 + .../main/assets/Roboto Medium/40704-40959.pbf | 3 + .../main/assets/Roboto Medium/4096-4351.pbf | 3 + .../main/assets/Roboto Medium/40960-41215.pbf | 3 + .../main/assets/Roboto Medium/41216-41471.pbf | 3 + .../main/assets/Roboto Medium/41472-41727.pbf | 3 + .../main/assets/Roboto Medium/41728-41983.pbf | 3 + .../main/assets/Roboto Medium/41984-42239.pbf | 3 + .../main/assets/Roboto Medium/42240-42495.pbf | 3 + .../main/assets/Roboto Medium/42496-42751.pbf | 3 + .../main/assets/Roboto Medium/42752-43007.pbf | 3 + .../main/assets/Roboto Medium/43008-43263.pbf | 3 + .../main/assets/Roboto Medium/43264-43519.pbf | 3 + .../main/assets/Roboto Medium/4352-4607.pbf | 3 + .../main/assets/Roboto Medium/43520-43775.pbf | 3 + .../main/assets/Roboto Medium/43776-44031.pbf | 3 + .../main/assets/Roboto Medium/44032-44287.pbf | 3 + .../main/assets/Roboto Medium/44288-44543.pbf | 3 + .../main/assets/Roboto Medium/44544-44799.pbf | 3 + .../main/assets/Roboto Medium/44800-45055.pbf | 3 + .../main/assets/Roboto Medium/45056-45311.pbf | 3 + .../main/assets/Roboto Medium/45312-45567.pbf | 3 + .../main/assets/Roboto Medium/45568-45823.pbf | 3 + .../main/assets/Roboto Medium/45824-46079.pbf | 3 + .../main/assets/Roboto Medium/4608-4863.pbf | 3 + .../main/assets/Roboto Medium/46080-46335.pbf | 3 + .../main/assets/Roboto Medium/46336-46591.pbf | 3 + .../main/assets/Roboto Medium/46592-46847.pbf | 3 + .../main/assets/Roboto Medium/46848-47103.pbf | 3 + .../main/assets/Roboto Medium/47104-47359.pbf | 3 + .../main/assets/Roboto Medium/47360-47615.pbf | 3 + .../main/assets/Roboto Medium/47616-47871.pbf | 3 + .../main/assets/Roboto Medium/47872-48127.pbf | 3 + .../main/assets/Roboto Medium/48128-48383.pbf | 3 + .../main/assets/Roboto Medium/48384-48639.pbf | 3 + .../main/assets/Roboto Medium/4864-5119.pbf | 3 + .../main/assets/Roboto Medium/48640-48895.pbf | 3 + .../main/assets/Roboto Medium/48896-49151.pbf | 3 + .../main/assets/Roboto Medium/49152-49407.pbf | 3 + .../main/assets/Roboto Medium/49408-49663.pbf | 3 + .../main/assets/Roboto Medium/49664-49919.pbf | 3 + .../main/assets/Roboto Medium/49920-50175.pbf | 3 + .../main/assets/Roboto Medium/50176-50431.pbf | 3 + .../main/assets/Roboto Medium/50432-50687.pbf | 3 + .../main/assets/Roboto Medium/50688-50943.pbf | 3 + .../main/assets/Roboto Medium/50944-51199.pbf | 3 + .../src/main/assets/Roboto Medium/512-767.pbf | Bin 0 -> 4691 bytes .../main/assets/Roboto Medium/5120-5375.pbf | 3 + .../main/assets/Roboto Medium/51200-51455.pbf | 3 + .../main/assets/Roboto Medium/51456-51711.pbf | 3 + .../main/assets/Roboto Medium/51712-51967.pbf | 3 + .../main/assets/Roboto Medium/51968-52223.pbf | 3 + .../main/assets/Roboto Medium/52224-52479.pbf | 3 + .../main/assets/Roboto Medium/52480-52735.pbf | 3 + .../main/assets/Roboto Medium/52736-52991.pbf | 3 + .../main/assets/Roboto Medium/52992-53247.pbf | 3 + .../main/assets/Roboto Medium/53248-53503.pbf | 3 + .../main/assets/Roboto Medium/53504-53759.pbf | 3 + .../main/assets/Roboto Medium/5376-5631.pbf | 3 + .../main/assets/Roboto Medium/53760-54015.pbf | 3 + .../main/assets/Roboto Medium/54016-54271.pbf | 3 + .../main/assets/Roboto Medium/54272-54527.pbf | 3 + .../main/assets/Roboto Medium/54528-54783.pbf | 3 + .../main/assets/Roboto Medium/54784-55039.pbf | 3 + .../main/assets/Roboto Medium/55040-55295.pbf | 3 + .../main/assets/Roboto Medium/55296-55551.pbf | 3 + .../main/assets/Roboto Medium/55552-55807.pbf | 3 + .../main/assets/Roboto Medium/55808-56063.pbf | 3 + .../main/assets/Roboto Medium/56064-56319.pbf | 3 + .../main/assets/Roboto Medium/5632-5887.pbf | 3 + .../main/assets/Roboto Medium/56320-56575.pbf | 3 + .../main/assets/Roboto Medium/56576-56831.pbf | 3 + .../main/assets/Roboto Medium/56832-57087.pbf | 3 + .../main/assets/Roboto Medium/57088-57343.pbf | 3 + .../main/assets/Roboto Medium/57344-57599.pbf | 3 + .../main/assets/Roboto Medium/57600-57855.pbf | 3 + .../main/assets/Roboto Medium/57856-58111.pbf | 3 + .../main/assets/Roboto Medium/58112-58367.pbf | 3 + .../main/assets/Roboto Medium/58368-58623.pbf | 3 + .../main/assets/Roboto Medium/58624-58879.pbf | 3 + .../main/assets/Roboto Medium/5888-6143.pbf | 3 + .../main/assets/Roboto Medium/58880-59135.pbf | 3 + .../main/assets/Roboto Medium/59136-59391.pbf | 3 + .../main/assets/Roboto Medium/59392-59647.pbf | 3 + .../main/assets/Roboto Medium/59648-59903.pbf | 3 + .../main/assets/Roboto Medium/59904-60159.pbf | 3 + .../main/assets/Roboto Medium/60160-60415.pbf | 3 + .../main/assets/Roboto Medium/60416-60671.pbf | 3 + .../main/assets/Roboto Medium/60672-60927.pbf | 3 + .../main/assets/Roboto Medium/60928-61183.pbf | 4 + .../main/assets/Roboto Medium/61184-61439.pbf | 3 + .../main/assets/Roboto Medium/6144-6399.pbf | 3 + .../main/assets/Roboto Medium/61440-61695.pbf | 3 + .../main/assets/Roboto Medium/61696-61951.pbf | 3 + .../main/assets/Roboto Medium/61952-62207.pbf | 3 + .../main/assets/Roboto Medium/62208-62463.pbf | 3 + .../main/assets/Roboto Medium/62464-62719.pbf | 3 + .../main/assets/Roboto Medium/62720-62975.pbf | 3 + .../main/assets/Roboto Medium/62976-63231.pbf | Bin 0 -> 183 bytes .../main/assets/Roboto Medium/63232-63487.pbf | 3 + .../main/assets/Roboto Medium/63488-63743.pbf | 3 + .../main/assets/Roboto Medium/63744-63999.pbf | 3 + .../main/assets/Roboto Medium/6400-6655.pbf | 3 + .../main/assets/Roboto Medium/64000-64255.pbf | 3 + .../main/assets/Roboto Medium/64256-64511.pbf | Bin 0 -> 2361 bytes .../main/assets/Roboto Medium/64512-64767.pbf | 3 + .../main/assets/Roboto Medium/64768-65023.pbf | 3 + .../main/assets/Roboto Medium/65024-65279.pbf | Bin 0 -> 84 bytes .../main/assets/Roboto Medium/65280-65535.pbf | Bin 0 -> 2043 bytes .../main/assets/Roboto Medium/6656-6911.pbf | 3 + .../main/assets/Roboto Medium/6912-7167.pbf | 3 + .../main/assets/Roboto Medium/7168-7423.pbf | 3 + .../main/assets/Roboto Medium/7424-7679.pbf | 3 + .../main/assets/Roboto Medium/768-1023.pbf | Bin 0 -> 37087 bytes .../main/assets/Roboto Medium/7680-7935.pbf | Bin 0 -> 59130 bytes .../main/assets/Roboto Medium/7936-8191.pbf | Bin 0 -> 773 bytes .../main/assets/Roboto Medium/8192-8447.pbf | Bin 0 -> 15233 bytes .../main/assets/Roboto Medium/8448-8703.pbf | Bin 0 -> 5394 bytes .../main/assets/Roboto Medium/8704-8959.pbf | Bin 0 -> 5355 bytes .../main/assets/Roboto Medium/8960-9215.pbf | 3 + .../main/assets/Roboto Medium/9216-9471.pbf | 3 + .../main/assets/Roboto Medium/9472-9727.pbf | Bin 0 -> 480 bytes .../main/assets/Roboto Medium/9728-9983.pbf | 3 + .../main/assets/Roboto Medium/9984-10239.pbf | 4 + .../src/main/assets/Roboto Regular/0-255.pbf | Bin 0 -> 81237 bytes .../main/assets/Roboto Regular/1024-1279.pbf | Bin 0 -> 131558 bytes .../assets/Roboto Regular/10240-10495.pbf | 3 + .../assets/Roboto Regular/10496-10751.pbf | 3 + .../assets/Roboto Regular/10752-11007.pbf | 3 + .../assets/Roboto Regular/11008-11263.pbf | 3 + .../assets/Roboto Regular/11264-11519.pbf | 3 + .../assets/Roboto Regular/11520-11775.pbf | 3 + .../assets/Roboto Regular/11776-12031.pbf | 3 + .../assets/Roboto Regular/12032-12287.pbf | 3 + .../assets/Roboto Regular/12288-12543.pbf | 3 + .../assets/Roboto Regular/12544-12799.pbf | 3 + .../main/assets/Roboto Regular/1280-1535.pbf | Bin 0 -> 24049 bytes .../assets/Roboto Regular/12800-13055.pbf | 3 + .../assets/Roboto Regular/13056-13311.pbf | 3 + .../assets/Roboto Regular/13312-13567.pbf | 3 + .../assets/Roboto Regular/13568-13823.pbf | 3 + .../assets/Roboto Regular/13824-14079.pbf | 3 + .../assets/Roboto Regular/14080-14335.pbf | 3 + .../assets/Roboto Regular/14336-14591.pbf | 3 + .../assets/Roboto Regular/14592-14847.pbf | 3 + .../assets/Roboto Regular/14848-15103.pbf | 3 + .../assets/Roboto Regular/15104-15359.pbf | 3 + .../main/assets/Roboto Regular/1536-1791.pbf | 3 + .../assets/Roboto Regular/15360-15615.pbf | 3 + .../assets/Roboto Regular/15616-15871.pbf | 3 + .../assets/Roboto Regular/15872-16127.pbf | 3 + .../assets/Roboto Regular/16128-16383.pbf | 3 + .../assets/Roboto Regular/16384-16639.pbf | 3 + .../assets/Roboto Regular/16640-16895.pbf | 3 + .../assets/Roboto Regular/16896-17151.pbf | 3 + .../assets/Roboto Regular/17152-17407.pbf | 3 + .../assets/Roboto Regular/17408-17663.pbf | 3 + .../assets/Roboto Regular/17664-17919.pbf | 3 + .../main/assets/Roboto Regular/1792-2047.pbf | 3 + .../assets/Roboto Regular/17920-18175.pbf | 3 + .../assets/Roboto Regular/18176-18431.pbf | 3 + .../assets/Roboto Regular/18432-18687.pbf | 3 + .../assets/Roboto Regular/18688-18943.pbf | 3 + .../assets/Roboto Regular/18944-19199.pbf | 3 + .../assets/Roboto Regular/19200-19455.pbf | 3 + .../assets/Roboto Regular/19456-19711.pbf | 3 + .../assets/Roboto Regular/19712-19967.pbf | 3 + .../assets/Roboto Regular/19968-20223.pbf | 3 + .../assets/Roboto Regular/20224-20479.pbf | 3 + .../main/assets/Roboto Regular/2048-2303.pbf | 3 + .../assets/Roboto Regular/20480-20735.pbf | 3 + .../assets/Roboto Regular/20736-20991.pbf | 3 + .../assets/Roboto Regular/20992-21247.pbf | 3 + .../assets/Roboto Regular/21248-21503.pbf | 3 + .../assets/Roboto Regular/21504-21759.pbf | 3 + .../assets/Roboto Regular/21760-22015.pbf | 3 + .../assets/Roboto Regular/22016-22271.pbf | 3 + .../assets/Roboto Regular/22272-22527.pbf | 3 + .../assets/Roboto Regular/22528-22783.pbf | 3 + .../assets/Roboto Regular/22784-23039.pbf | 3 + .../main/assets/Roboto Regular/2304-2559.pbf | 3 + .../assets/Roboto Regular/23040-23295.pbf | 3 + .../assets/Roboto Regular/23296-23551.pbf | 3 + .../assets/Roboto Regular/23552-23807.pbf | 3 + .../assets/Roboto Regular/23808-24063.pbf | 3 + .../assets/Roboto Regular/24064-24319.pbf | 3 + .../assets/Roboto Regular/24320-24575.pbf | 3 + .../assets/Roboto Regular/24576-24831.pbf | 3 + .../assets/Roboto Regular/24832-25087.pbf | 3 + .../assets/Roboto Regular/25088-25343.pbf | 3 + .../assets/Roboto Regular/25344-25599.pbf | 3 + .../main/assets/Roboto Regular/256-511.pbf | Bin 0 -> 74366 bytes .../main/assets/Roboto Regular/2560-2815.pbf | 3 + .../assets/Roboto Regular/25600-25855.pbf | 3 + .../assets/Roboto Regular/25856-26111.pbf | 3 + .../assets/Roboto Regular/26112-26367.pbf | 3 + .../assets/Roboto Regular/26368-26623.pbf | 3 + .../assets/Roboto Regular/26624-26879.pbf | 3 + .../assets/Roboto Regular/26880-27135.pbf | 3 + .../assets/Roboto Regular/27136-27391.pbf | 3 + .../assets/Roboto Regular/27392-27647.pbf | 3 + .../assets/Roboto Regular/27648-27903.pbf | 3 + .../assets/Roboto Regular/27904-28159.pbf | 3 + .../main/assets/Roboto Regular/2816-3071.pbf | 3 + .../assets/Roboto Regular/28160-28415.pbf | 3 + .../assets/Roboto Regular/28416-28671.pbf | 3 + .../assets/Roboto Regular/28672-28927.pbf | 3 + .../assets/Roboto Regular/28928-29183.pbf | 3 + .../assets/Roboto Regular/29184-29439.pbf | 3 + .../assets/Roboto Regular/29440-29695.pbf | 3 + .../assets/Roboto Regular/29696-29951.pbf | 3 + .../assets/Roboto Regular/29952-30207.pbf | 3 + .../assets/Roboto Regular/30208-30463.pbf | 3 + .../assets/Roboto Regular/30464-30719.pbf | 3 + .../main/assets/Roboto Regular/3072-3327.pbf | 3 + .../assets/Roboto Regular/30720-30975.pbf | 3 + .../assets/Roboto Regular/30976-31231.pbf | 3 + .../assets/Roboto Regular/31232-31487.pbf | 3 + .../assets/Roboto Regular/31488-31743.pbf | 3 + .../assets/Roboto Regular/31744-31999.pbf | 3 + .../assets/Roboto Regular/32000-32255.pbf | 3 + .../assets/Roboto Regular/32256-32511.pbf | 3 + .../assets/Roboto Regular/32512-32767.pbf | 3 + .../assets/Roboto Regular/32768-33023.pbf | 3 + .../assets/Roboto Regular/33024-33279.pbf | 3 + .../main/assets/Roboto Regular/3328-3583.pbf | 3 + .../assets/Roboto Regular/33280-33535.pbf | 3 + .../assets/Roboto Regular/33536-33791.pbf | 3 + .../assets/Roboto Regular/33792-34047.pbf | 3 + .../assets/Roboto Regular/34048-34303.pbf | 3 + .../assets/Roboto Regular/34304-34559.pbf | 3 + .../assets/Roboto Regular/34560-34815.pbf | 3 + .../assets/Roboto Regular/34816-35071.pbf | 3 + .../assets/Roboto Regular/35072-35327.pbf | 3 + .../assets/Roboto Regular/35328-35583.pbf | 3 + .../assets/Roboto Regular/35584-35839.pbf | 3 + .../main/assets/Roboto Regular/3584-3839.pbf | 3 + .../assets/Roboto Regular/35840-36095.pbf | 3 + .../assets/Roboto Regular/36096-36351.pbf | 3 + .../assets/Roboto Regular/36352-36607.pbf | 3 + .../assets/Roboto Regular/36608-36863.pbf | 3 + .../assets/Roboto Regular/36864-37119.pbf | 3 + .../assets/Roboto Regular/37120-37375.pbf | 3 + .../assets/Roboto Regular/37376-37631.pbf | 3 + .../assets/Roboto Regular/37632-37887.pbf | 3 + .../assets/Roboto Regular/37888-38143.pbf | 3 + .../assets/Roboto Regular/38144-38399.pbf | 3 + .../main/assets/Roboto Regular/3840-4095.pbf | 3 + .../assets/Roboto Regular/38400-38655.pbf | 3 + .../assets/Roboto Regular/38656-38911.pbf | 3 + .../assets/Roboto Regular/38912-39167.pbf | 3 + .../assets/Roboto Regular/39168-39423.pbf | 3 + .../assets/Roboto Regular/39424-39679.pbf | 3 + .../assets/Roboto Regular/39680-39935.pbf | 3 + .../assets/Roboto Regular/39936-40191.pbf | 3 + .../assets/Roboto Regular/40192-40447.pbf | 3 + .../assets/Roboto Regular/40448-40703.pbf | 3 + .../assets/Roboto Regular/40704-40959.pbf | 3 + .../main/assets/Roboto Regular/4096-4351.pbf | 3 + .../assets/Roboto Regular/40960-41215.pbf | 3 + .../assets/Roboto Regular/41216-41471.pbf | 3 + .../assets/Roboto Regular/41472-41727.pbf | 3 + .../assets/Roboto Regular/41728-41983.pbf | 3 + .../assets/Roboto Regular/41984-42239.pbf | 3 + .../assets/Roboto Regular/42240-42495.pbf | 3 + .../assets/Roboto Regular/42496-42751.pbf | 3 + .../assets/Roboto Regular/42752-43007.pbf | Bin 0 -> 1899 bytes .../assets/Roboto Regular/43008-43263.pbf | 3 + .../assets/Roboto Regular/43264-43519.pbf | 3 + .../main/assets/Roboto Regular/4352-4607.pbf | 3 + .../assets/Roboto Regular/43520-43775.pbf | 3 + .../assets/Roboto Regular/43776-44031.pbf | Bin 0 -> 571 bytes .../assets/Roboto Regular/44032-44287.pbf | 3 + .../assets/Roboto Regular/44288-44543.pbf | 3 + .../assets/Roboto Regular/44544-44799.pbf | 3 + .../assets/Roboto Regular/44800-45055.pbf | 3 + .../assets/Roboto Regular/45056-45311.pbf | 3 + .../assets/Roboto Regular/45312-45567.pbf | 3 + .../assets/Roboto Regular/45568-45823.pbf | 3 + .../assets/Roboto Regular/45824-46079.pbf | 3 + .../main/assets/Roboto Regular/4608-4863.pbf | 3 + .../assets/Roboto Regular/46080-46335.pbf | 3 + .../assets/Roboto Regular/46336-46591.pbf | 3 + .../assets/Roboto Regular/46592-46847.pbf | 3 + .../assets/Roboto Regular/46848-47103.pbf | 3 + .../assets/Roboto Regular/47104-47359.pbf | 3 + .../assets/Roboto Regular/47360-47615.pbf | 3 + .../assets/Roboto Regular/47616-47871.pbf | 3 + .../assets/Roboto Regular/47872-48127.pbf | 3 + .../assets/Roboto Regular/48128-48383.pbf | 3 + .../assets/Roboto Regular/48384-48639.pbf | 3 + .../main/assets/Roboto Regular/4864-5119.pbf | 3 + .../assets/Roboto Regular/48640-48895.pbf | 3 + .../assets/Roboto Regular/48896-49151.pbf | 3 + .../assets/Roboto Regular/49152-49407.pbf | 3 + .../assets/Roboto Regular/49408-49663.pbf | 3 + .../assets/Roboto Regular/49664-49919.pbf | 3 + .../assets/Roboto Regular/49920-50175.pbf | 3 + .../assets/Roboto Regular/50176-50431.pbf | 3 + .../assets/Roboto Regular/50432-50687.pbf | 3 + .../assets/Roboto Regular/50688-50943.pbf | 3 + .../assets/Roboto Regular/50944-51199.pbf | 3 + .../main/assets/Roboto Regular/512-767.pbf | Bin 0 -> 4559 bytes .../main/assets/Roboto Regular/5120-5375.pbf | 3 + .../assets/Roboto Regular/51200-51455.pbf | 3 + .../assets/Roboto Regular/51456-51711.pbf | 3 + .../assets/Roboto Regular/51712-51967.pbf | 3 + .../assets/Roboto Regular/51968-52223.pbf | 3 + .../assets/Roboto Regular/52224-52479.pbf | 3 + .../assets/Roboto Regular/52480-52735.pbf | 3 + .../assets/Roboto Regular/52736-52991.pbf | 3 + .../assets/Roboto Regular/52992-53247.pbf | 3 + .../assets/Roboto Regular/53248-53503.pbf | 3 + .../assets/Roboto Regular/53504-53759.pbf | 3 + .../main/assets/Roboto Regular/5376-5631.pbf | 3 + .../assets/Roboto Regular/53760-54015.pbf | 3 + .../assets/Roboto Regular/54016-54271.pbf | 3 + .../assets/Roboto Regular/54272-54527.pbf | 3 + .../assets/Roboto Regular/54528-54783.pbf | 3 + .../assets/Roboto Regular/54784-55039.pbf | 3 + .../assets/Roboto Regular/55040-55295.pbf | 3 + .../assets/Roboto Regular/55296-55551.pbf | 3 + .../assets/Roboto Regular/55552-55807.pbf | 3 + .../assets/Roboto Regular/55808-56063.pbf | 3 + .../assets/Roboto Regular/56064-56319.pbf | 3 + .../main/assets/Roboto Regular/5632-5887.pbf | 3 + .../assets/Roboto Regular/56320-56575.pbf | 3 + .../assets/Roboto Regular/56576-56831.pbf | 3 + .../assets/Roboto Regular/56832-57087.pbf | 3 + .../assets/Roboto Regular/57088-57343.pbf | 3 + .../assets/Roboto Regular/57344-57599.pbf | 3 + .../assets/Roboto Regular/57600-57855.pbf | 3 + .../assets/Roboto Regular/57856-58111.pbf | 3 + .../assets/Roboto Regular/58112-58367.pbf | 3 + .../assets/Roboto Regular/58368-58623.pbf | 3 + .../assets/Roboto Regular/58624-58879.pbf | 3 + .../main/assets/Roboto Regular/5888-6143.pbf | 3 + .../assets/Roboto Regular/58880-59135.pbf | 3 + .../assets/Roboto Regular/59136-59391.pbf | 3 + .../assets/Roboto Regular/59392-59647.pbf | 3 + .../assets/Roboto Regular/59648-59903.pbf | 3 + .../assets/Roboto Regular/59904-60159.pbf | 3 + .../assets/Roboto Regular/60160-60415.pbf | 3 + .../assets/Roboto Regular/60416-60671.pbf | 3 + .../assets/Roboto Regular/60672-60927.pbf | 3 + .../assets/Roboto Regular/60928-61183.pbf | 3 + .../assets/Roboto Regular/61184-61439.pbf | 3 + .../main/assets/Roboto Regular/6144-6399.pbf | 3 + .../assets/Roboto Regular/61440-61695.pbf | 3 + .../assets/Roboto Regular/61696-61951.pbf | 3 + .../assets/Roboto Regular/61952-62207.pbf | 3 + .../assets/Roboto Regular/62208-62463.pbf | 3 + .../assets/Roboto Regular/62464-62719.pbf | 3 + .../assets/Roboto Regular/62720-62975.pbf | 3 + .../assets/Roboto Regular/62976-63231.pbf | 3 + .../assets/Roboto Regular/63232-63487.pbf | 3 + .../assets/Roboto Regular/63488-63743.pbf | 3 + .../assets/Roboto Regular/63744-63999.pbf | 3 + .../main/assets/Roboto Regular/6400-6655.pbf | 3 + .../assets/Roboto Regular/64000-64255.pbf | 3 + .../assets/Roboto Regular/64256-64511.pbf | Bin 0 -> 15620 bytes .../assets/Roboto Regular/64512-64767.pbf | 3 + .../assets/Roboto Regular/64768-65023.pbf | 3 + .../assets/Roboto Regular/65024-65279.pbf | Bin 0 -> 104 bytes .../assets/Roboto Regular/65280-65535.pbf | Bin 0 -> 2063 bytes .../main/assets/Roboto Regular/6656-6911.pbf | 3 + .../main/assets/Roboto Regular/6912-7167.pbf | 3 + .../main/assets/Roboto Regular/7168-7423.pbf | 3 + .../main/assets/Roboto Regular/7424-7679.pbf | 3 + .../main/assets/Roboto Regular/768-1023.pbf | Bin 0 -> 37852 bytes .../main/assets/Roboto Regular/7680-7935.pbf | Bin 0 -> 58681 bytes .../main/assets/Roboto Regular/7936-8191.pbf | Bin 0 -> 930 bytes .../main/assets/Roboto Regular/8192-8447.pbf | Bin 0 -> 22915 bytes .../main/assets/Roboto Regular/8448-8703.pbf | Bin 0 -> 5704 bytes .../main/assets/Roboto Regular/8704-8959.pbf | Bin 0 -> 5692 bytes .../main/assets/Roboto Regular/8960-9215.pbf | 3 + .../main/assets/Roboto Regular/9216-9471.pbf | 3 + .../main/assets/Roboto Regular/9472-9727.pbf | Bin 0 -> 500 bytes .../main/assets/Roboto Regular/9728-9983.pbf | 3 + .../main/assets/Roboto Regular/9984-10239.pbf | 4 + .../core/mapbox/src/main/assets/sprites.json | 261 + .../core/mapbox/src/main/assets/sprites.png | Bin 0 -> 17154 bytes .../mapbox/src/main/assets/sprites@2x.json | 261 + .../mapbox/src/main/assets/sprites@2x.png | Bin 0 -> 38683 bytes .../assets/style-mapbox-outdoors-v12.json | 13845 ++++++++++++++++ .../assets/style-microg-normal-mapbox.json | 4392 +++++ .../assets/style-microg-normal-stadia.json | 4735 ++++++ .../assets/style-microg-satellite-mapbox.json | 3489 ++++ .../assets/style-microg-satellite-stadia.json | 3632 ++++ .../main/assets/style-stadia-outdoors.json | 5912 +++++++ .../gms/maps_dynamite/ModuleDescriptor.java | 14 + .../gms/maps/internal/CreatorImpl.java | 102 + .../android/accounts/v1/MapboxAccounts.java | 28 + .../gms/maps/mapbox/AbstractGoogleMap.kt | 173 + .../maps/mapbox/CameraBoundsWithSizeUpdate.kt | 107 + .../gms/maps/mapbox/CameraUpdateFactory.kt | 167 + .../gms/maps/mapbox/GoogleLocationEngine.kt | 69 + .../org/microg/gms/maps/mapbox/GoogleMap.kt | 1033 ++ .../microg/gms/maps/mapbox/LiteGoogleMap.kt | 676 + .../org/microg/gms/maps/mapbox/MapFragment.kt | 133 + .../org/microg/gms/maps/mapbox/MapView.kt | 88 + .../org/microg/gms/maps/mapbox/Pattern.kt | 88 + .../org/microg/gms/maps/mapbox/Projection.kt | 132 + .../gms/maps/mapbox/SourceLocationEngine.kt | 69 + .../maps/mapbox/StreetViewPanoramaFragment.kt | 63 + .../gms/maps/mapbox/StreetViewPanoramaView.kt | 54 + .../org/microg/gms/maps/mapbox/Styles.kt | 459 + .../org/microg/gms/maps/mapbox/UiSettings.kt | 201 + .../gms/maps/mapbox/model/BitmapDescriptor.kt | 63 + .../mapbox/model/BitmapDescriptorFactory.kt | 211 + .../microg/gms/maps/mapbox/model/Circle.kt | 339 + .../gms/maps/mapbox/model/GroundOverlay.kt | 174 + .../gms/maps/mapbox/model/InfoWindow.kt | 171 + .../microg/gms/maps/mapbox/model/Marker.kt | 334 + .../microg/gms/maps/mapbox/model/Markup.kt | 45 + .../microg/gms/maps/mapbox/model/Polygon.kt | 281 + .../microg/gms/maps/mapbox/model/Polyline.kt | 204 + .../gms/maps/mapbox/model/TileOverlay.kt | 64 + .../gms/maps/mapbox/utils/MapContext.kt | 81 + .../gms/maps/mapbox/utils/MultiArchLoader.kt | 85 + .../gms/maps/mapbox/utils/typeConverter.kt | 101 + .../res/drawable-hdpi/maps_default_marker.png | Bin 0 -> 1974 bytes .../res/drawable-ldpi/maps_default_marker.png | Bin 0 -> 946 bytes .../res/drawable-mdpi/maps_default_marker.png | Bin 0 -> 1277 bytes .../drawable-xhdpi/maps_default_marker.png | Bin 0 -> 2745 bytes .../drawable-xxhdpi/maps_default_marker.png | Bin 0 -> 4277 bytes .../drawable-xxxhdpi/maps_default_marker.png | Bin 0 -> 5890 bytes .../src/main/res/drawable/location_dot.xml | 5 + .../main/res/drawable/maps_default_bubble.xml | 33 + .../res/layout/maps_default_bubble_layout.xml | 34 + play-services-maps/core/vtm/build.gradle | 79 + .../core/vtm/microg-theme/build.gradle | 16 + .../resources/assets/styles/microg.xml | 546 + .../resources/assets/symbols/dot_white.svg | 13 + .../assets/symbols/transit/train_station.svg | 7 + .../symbols/transit/train_station_small.svg | 7 + .../src/org/oscim/theme/MicrogThemes.java | 41 + .../core/vtm/src/main/AndroidManifest.xml | 39 + .../gms/maps/internal/CreatorImpl.java | 102 + .../maps/vtm/ApplicationContextWrapper.java | 50 + .../org/microg/gms/maps/vtm/BackendMap.java | 340 + .../microg/gms/maps/vtm/BackendMapView.java | 156 + .../microg/gms/maps/vtm/ContainerLayout.java | 34 + .../gms/maps/vtm/GmsMapsTypeHelper.java | 90 + .../microg/gms/maps/vtm/GoogleMapImpl.java | 735 + .../microg/gms/maps/vtm/MapFragmentImpl.java | 180 + .../org/microg/gms/maps/vtm/MapViewImpl.java | 151 + .../microg/gms/maps/vtm/ProjectionImpl.java | 59 + .../gms/maps/vtm/ResourcesContainer.java | 35 + .../vtm/StreetViewPanoramaFragmentImpl.java | 97 + .../maps/vtm/StreetViewPanoramaViewImpl.java | 81 + .../microg/gms/maps/vtm/UiSettingsImpl.java | 170 + .../vtm/bitmap/AbstractBitmapDescriptor.java | 41 + .../vtm/bitmap/AssetBitmapDescriptor.java | 41 + .../vtm/bitmap/BitmapBitmapDescriptor.java | 38 + .../bitmap/BitmapDescriptorFactoryImpl.java | 72 + .../maps/vtm/bitmap/BitmapDescriptorImpl.java | 93 + .../vtm/bitmap/DefaultBitmapDescriptor.java | 105 + .../maps/vtm/bitmap/FileBitmapDescriptor.java | 34 + .../maps/vtm/bitmap/PathBitmapDescriptor.java | 22 + .../vtm/bitmap/ResourceBitmapDescriptor.java | 55 + .../gms/maps/vtm/camera/CameraUpdate.java | 24 + .../vtm/camera/CameraUpdateFactoryImpl.java | 203 + .../vtm/camera/MapPositionCameraUpdate.java | 44 + .../gms/maps/vtm/camera/NoCameraUpdate.java | 31 + .../gms/maps/vtm/data/SharedTileCache.java | 138 + .../gms/maps/vtm/data/SharedTileProvider.java | 94 + .../gms/maps/vtm/markup/CircleImpl.java | 214 + .../maps/vtm/markup/ClearableVectorLayer.java | 30 + .../gms/maps/vtm/markup/DrawableMarkup.java | 26 + .../maps/vtm/markup/GroundOverlayImpl.java | 165 + .../gms/maps/vtm/markup/InfoWindow.java | 165 + .../gms/maps/vtm/markup/MarkerImpl.java | 289 + .../gms/maps/vtm/markup/MarkerItemMarkup.java | 33 + .../microg/gms/maps/vtm/markup/Markup.java | 41 + .../gms/maps/vtm/markup/PolygonImpl.java | 242 + .../gms/maps/vtm/markup/PolylineImpl.java | 237 + .../gms/maps/vtm/markup/TileOverlayImpl.java | 87 + .../main/res/drawable/maps_default_marker.png | Bin 0 -> 4303 bytes .../res/drawable/maps_default_window.9.png | Bin 0 -> 1335 bytes .../core/vtm/src/main/res/drawable/nop.png | Bin 0 -> 169 bytes .../src/main/AndroidManifest.xml | 22 + .../android/gms/maps/GoogleMapOptions.aidl | 3 + .../gms/maps/StreetViewPanoramaOptions.aidl | 8 + .../ICameraUpdateFactoryDelegate.aidl | 20 + .../maps/internal/ICancelableCallback.aidl | 6 + .../android/gms/maps/internal/ICreator.aidl | 24 + .../gms/maps/internal/IGoogleMapDelegate.aidl | 152 + .../gms/maps/internal/IInfoWindowAdapter.aidl | 9 + .../internal/ILocationSourceDelegate.aidl | 8 + .../maps/internal/IMapFragmentDelegate.aidl | 27 + .../gms/maps/internal/IMapViewDelegate.aidl | 23 + .../internal/IOnCameraChangeListener.aidl | 7 + .../maps/internal/IOnCameraIdleListener.aidl | 5 + .../IOnCameraMoveCanceledListener.aidl | 5 + .../maps/internal/IOnCameraMoveListener.aidl | 5 + .../IOnCameraMoveStartedListener.aidl | 5 + .../maps/internal/IOnCircleClickListener.aidl | 7 + .../IOnGroundOverlayClickListener.aidl | 7 + .../IOnIndoorStateChangeListener.aidl | 6 + .../internal/IOnInfoWindowClickListener.aidl | 7 + .../internal/IOnInfoWindowCloseListener.aidl | 7 + .../IOnInfoWindowLongClickListener.aidl | 7 + .../internal/IOnLocationChangeListener.aidl | 7 + .../maps/internal/IOnMapClickListener.aidl | 7 + .../maps/internal/IOnMapLoadedCallback.aidl | 5 + .../internal/IOnMapLongClickListener.aidl | 7 + .../maps/internal/IOnMapReadyCallback.aidl | 7 + .../maps/internal/IOnMarkerClickListener.aidl | 7 + .../maps/internal/IOnMarkerDragListener.aidl | 9 + .../IOnMyLocationButtonClickListener.aidl | 5 + .../internal/IOnMyLocationChangeListener.aidl | 7 + .../internal/IOnMyLocationClickListener.aidl | 7 + .../internal/IOnPolygonClickListener.aidl | 7 + .../internal/IOnPolylineClickListener.aidl | 7 + ...treetViewPanoramaCameraChangeListener.aidl | 12 + .../IOnStreetViewPanoramaChangeListener.aidl | 12 + .../IOnStreetViewPanoramaClickListener.aidl | 12 + ...OnStreetViewPanoramaLongClickListener.aidl | 12 + .../IOnStreetViewPanoramaReadyCallback.aidl | 12 + .../maps/internal/IProjectionDelegate.aidl | 11 + .../maps/internal/ISnapshotReadyCallback.aidl | 9 + .../internal/IStreetViewPanoramaDelegate.aidl | 42 + .../IStreetViewPanoramaFragmentDelegate.aidl | 30 + .../IStreetViewPanoramaViewDelegate.aidl | 26 + .../maps/internal/IUiSettingsDelegate.aidl | 25 + .../gms/maps/model/CameraPosition.aidl | 3 + .../google/android/gms/maps/model/Cap.aidl | 3 + .../android/gms/maps/model/CircleOptions.aidl | 3 + .../google/android/gms/maps/model/Dash.aidl | 3 + .../google/android/gms/maps/model/Dot.aidl | 3 + .../google/android/gms/maps/model/Gap.aidl | 3 + .../gms/maps/model/GroundOverlayOptions.aidl | 3 + .../google/android/gms/maps/model/LatLng.aidl | 3 + .../android/gms/maps/model/LatLngBounds.aidl | 3 + .../gms/maps/model/MapStyleOptions.aidl | 3 + .../android/gms/maps/model/MarkerOptions.aidl | 3 + .../android/gms/maps/model/PatternItem.aidl | 3 + .../gms/maps/model/PolygonOptions.aidl | 3 + .../gms/maps/model/PolylineOptions.aidl | 3 + .../maps/model/StreetViewPanoramaCamera.aidl | 8 + .../model/StreetViewPanoramaLocation.aidl | 8 + .../model/StreetViewPanoramaOrientation.aidl | 8 + .../gms/maps/model/StreetViewSource.aidl | 8 + .../android/gms/maps/model/StyleSpan.aidl | 3 + .../google/android/gms/maps/model/Tile.aidl | 3 + .../gms/maps/model/TileOverlayOptions.aidl | 3 + .../android/gms/maps/model/VisibleRegion.aidl | 3 + .../IBitmapDescriptorFactoryDelegate.aidl | 14 + .../maps/model/internal/ICircleDelegate.aidl | 32 + .../internal/IGroundOverlayDelegate.aidl | 33 + .../internal/IIndoorBuildingDelegate.aidl | 10 + .../model/internal/IIndoorLevelDelegate.aidl | 9 + .../maps/model/internal/IMarkerDelegate.aidl | 37 + .../maps/model/internal/IPolygonDelegate.aidl | 41 + .../model/internal/IPolylineDelegate.aidl | 40 + .../model/internal/ITileOverlayDelegate.aidl | 17 + .../model/internal/ITileProviderDelegate.aidl | 7 + .../google/android/gms/maps/CameraUpdate.java | 27 + .../android/gms/maps/CameraUpdateFactory.java | 48 + .../google/android/gms/maps/GoogleMap.java | 199 + .../android/gms/maps/GoogleMapOptions.java | 479 + .../google/android/gms/maps/MapFragment.java | 11 + .../com/google/android/gms/maps/MapView.java | 155 + .../android/gms/maps/MapsInitializer.java | 120 + .../android/gms/maps/OnMapReadyCallback.java | 42 + .../maps/OnMapsSdkInitializedCallback.java | 23 + .../OnStreetViewPanoramaReadyCallback.java | 29 + .../android/gms/maps/StreetViewPanorama.java | 421 + .../gms/maps/StreetViewPanoramaFragment.java | 11 + .../gms/maps/StreetViewPanoramaOptions.java | 308 + .../gms/maps/StreetViewPanoramaView.java | 122 + .../gms/maps/model/BitmapDescriptor.java | 38 + .../maps/model/BitmapDescriptorFactory.java | 24 + .../android/gms/maps/model/ButtCap.java | 31 + .../gms/maps/model/CameraPosition.java | 249 + .../google/android/gms/maps/model/Cap.java | 104 + .../google/android/gms/maps/model/Circle.java | 185 + .../android/gms/maps/model/CircleOptions.java | 268 + .../android/gms/maps/model/CustomCap.java | 51 + .../google/android/gms/maps/model/Dash.java | 39 + .../google/android/gms/maps/model/Dot.java | 32 + .../google/android/gms/maps/model/Gap.java | 39 + .../gms/maps/model/GroundOverlayOptions.java | 354 + .../android/gms/maps/model/JointType.java | 27 + .../google/android/gms/maps/model/LatLng.java | 119 + .../android/gms/maps/model/LatLngBounds.java | 227 + .../gms/maps/model/MapStyleOptions.java | 47 + .../android/gms/maps/model/MarkerOptions.java | 341 + .../android/gms/maps/model/PatternItem.java | 56 + .../gms/maps/model/PolygonOptions.java | 302 + .../gms/maps/model/PolylineOptions.java | 345 + .../android/gms/maps/model/RoundCap.java | 29 + .../maps/model/RuntimeRemoteException.java | 18 + .../android/gms/maps/model/SquareCap.java | 29 + .../android/gms/maps/model/StampStyle.java | 18 + .../maps/model/StreetViewPanoramaCamera.java | 199 + .../maps/model/StreetViewPanoramaLink.java | 72 + .../model/StreetViewPanoramaLocation.java | 87 + .../model/StreetViewPanoramaOrientation.java | 150 + .../gms/maps/model/StreetViewSource.java | 81 + .../android/gms/maps/model/StrokeStyle.java | 23 + .../android/gms/maps/model/StyleSpan.java | 25 + .../google/android/gms/maps/model/Tile.java | 69 + .../gms/maps/model/TileOverlayOptions.java | 170 + .../android/gms/maps/model/TileProvider.java | 41 + .../android/gms/maps/model/VisibleRegion.java | 67 + .../android/gms/maps/model/package-info.java | 20 + .../google/android/gms/maps/package-info.java | 11 + .../microg/gms/maps/MapLifecycleDelegate.java | 14 + .../org/microg/gms/maps/MapViewDelegate.java | 162 + .../gms/maps/MapViewLifecycleHelper.java | 57 + .../org/microg/gms/maps/MapsBundleHelper.java | 65 + .../microg/gms/maps/MapsContextLoader.java | 85 + .../gms/maps/StreetViewLifecycleDelegate.java | 14 + .../maps/StreetViewPanoramaViewDelegate.java | 141 + ...StreetViewPanoramaViewLifecycleHelper.java | 57 + .../src/main/res/values/maps_attrs.xml | 41 + play-services/build.gradle | 3 +- settings.gradle | 9 + 1343 files changed, 86653 insertions(+), 8 deletions(-) create mode 100644 play-services-base/core/src/main/kotlin/org/microg/gms/utils/SingleInstanceUtil.kt create mode 100644 play-services-base/core/src/main/res/drawable/ic_radio_checked.xml create mode 100644 play-services-base/core/src/main/res/drawable/ic_radio_unchecked.xml create mode 100644 play-services-location/build.gradle create mode 100644 play-services-location/core/base/build.gradle create mode 100644 play-services-location/core/base/src/main/AndroidManifest.xml create mode 100644 play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt create mode 100644 play-services-location/core/base/src/main/kotlin/org/microg/gms/location/extensions.kt create mode 100644 play-services-location/core/base/src/main/kotlin/org/microg/gms/location/network/OnlineSource.kt create mode 100644 play-services-location/core/build.gradle create mode 100644 play-services-location/core/provider/build.gradle create mode 100644 play-services-location/core/provider/src/main/AndroidManifest.xml create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/DatabaseExportFileProvider.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationDatabase.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkDetails.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsCallback.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/extensions.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/BluetoothBeacon.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/CellTower.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/Fallback.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeolocateRequest.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeolocateResponse.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitItem.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitPosition.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitRequest.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitSource.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/RadioType.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/RawGeolocateEntry.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ResponseError.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ResponseLocation.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ServiceException.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/WifiAccessPoint.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/extensions.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsSource.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderService.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/extensions.kt create mode 100644 play-services-location/core/provider/src/main/res/xml/location_exported_files.xml create mode 100644 play-services-location/core/src/huawei/AndroidManifest.xml create mode 100644 play-services-location/core/src/huawei/kotlin/org/microg/gms/location/manager/AskPermissionNotificationActivity.kt create mode 100644 play-services-location/core/src/huawei/res/drawable-zh/permission_step_1.jpg create mode 100644 play-services-location/core/src/huawei/res/drawable-zh/permission_step_2.jpg create mode 100644 play-services-location/core/src/huawei/res/drawable/arrow.png create mode 100644 play-services-location/core/src/huawei/res/drawable/ic_app.png create mode 100644 play-services-location/core/src/huawei/res/drawable/ic_permission_notification.xml create mode 100644 play-services-location/core/src/huawei/res/drawable/permission_step_1.jpg create mode 100644 play-services-location/core/src/huawei/res/drawable/permission_step_2.jpg create mode 100644 play-services-location/core/src/huawei/res/layout/extended_permission_request.xml create mode 100644 play-services-location/core/src/huawei/res/values-zh-rCN/strings.xml create mode 100644 play-services-location/core/src/huawei/res/values/strings.xml create mode 100644 play-services-location/core/src/main/AndroidManifest.xml create mode 100644 play-services-location/core/src/main/kotlin/com/google/android/location/settings/LocationHistorySettingsActivity.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/AskPermissionActivity.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/LocationAppsDatabase.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/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/core/src/main/kotlin/org/microg/gms/location/settings/DetailedLocationSettingsStates.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/settings/GoogleLocationSettingsActivity.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/settings/LocationSettingsCheckerActivity.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/ConfigurationRequiredReceiver.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationAllAppsFragment.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationAppFragment.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationMapPreference.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/LocationPreferencesFragment.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/ui/extensions.kt create mode 100644 play-services-location/core/src/main/res/drawable/ic_gps.xml create mode 100644 play-services-location/core/src/main/res/drawable/ic_location.xml create mode 100644 play-services-location/core/src/main/res/layout/location_settings_dialog.xml create mode 100644 play-services-location/core/src/main/res/layout/location_settings_dialog_item.xml create mode 100644 play-services-location/core/src/main/res/layout/preference_full_container.xml create mode 100644 play-services-location/core/src/main/res/layout/preference_location_custom_url.xml create mode 100644 play-services-location/core/src/main/res/layout/preference_location_online_source.xml create mode 100644 play-services-location/core/src/main/res/navigation/nav_location.xml create mode 100644 play-services-location/core/src/main/res/values-ar/strings.xml create mode 100644 play-services-location/core/src/main/res/values-ast/strings.xml create mode 100644 play-services-location/core/src/main/res/values-az/strings.xml create mode 100644 play-services-location/core/src/main/res/values-be/strings.xml create mode 100644 play-services-location/core/src/main/res/values-bn/strings.xml create mode 100644 play-services-location/core/src/main/res/values-ca/strings.xml create mode 100644 play-services-location/core/src/main/res/values-cs/strings.xml create mode 100644 play-services-location/core/src/main/res/values-de/strings.xml create mode 100644 play-services-location/core/src/main/res/values-eo/strings.xml create mode 100644 play-services-location/core/src/main/res/values-es/strings.xml create mode 100644 play-services-location/core/src/main/res/values-fa/strings.xml create mode 100644 play-services-location/core/src/main/res/values-fi/strings.xml create mode 100644 play-services-location/core/src/main/res/values-fil/strings.xml create mode 100644 play-services-location/core/src/main/res/values-fr/strings.xml create mode 100644 play-services-location/core/src/main/res/values-ga/strings.xml create mode 100644 play-services-location/core/src/main/res/values-hi/strings.xml create mode 100644 play-services-location/core/src/main/res/values-hu/strings.xml create mode 100644 play-services-location/core/src/main/res/values-in/strings.xml create mode 100644 play-services-location/core/src/main/res/values-is/strings.xml create mode 100644 play-services-location/core/src/main/res/values-it/strings.xml create mode 100644 play-services-location/core/src/main/res/values-iw/strings.xml create mode 100644 play-services-location/core/src/main/res/values-ja/strings.xml create mode 100644 play-services-location/core/src/main/res/values-ko/strings.xml create mode 100644 play-services-location/core/src/main/res/values-lt/strings.xml create mode 100644 play-services-location/core/src/main/res/values-lv/strings.xml create mode 100644 play-services-location/core/src/main/res/values-lzh/strings.xml create mode 100644 play-services-location/core/src/main/res/values-ml/strings.xml create mode 100644 play-services-location/core/src/main/res/values-mn/strings.xml create mode 100644 play-services-location/core/src/main/res/values-nb-rNO/strings.xml create mode 100644 play-services-location/core/src/main/res/values-nl/strings.xml create mode 100644 play-services-location/core/src/main/res/values-pl/strings.xml create mode 100644 play-services-location/core/src/main/res/values-pt-rBR/strings.xml create mode 100644 play-services-location/core/src/main/res/values-pt/strings.xml create mode 100644 play-services-location/core/src/main/res/values-ro/strings.xml create mode 100644 play-services-location/core/src/main/res/values-ru/strings.xml create mode 100644 play-services-location/core/src/main/res/values-si/strings.xml create mode 100644 play-services-location/core/src/main/res/values-sl/strings.xml create mode 100644 play-services-location/core/src/main/res/values-sr/strings.xml create mode 100644 play-services-location/core/src/main/res/values-sv/strings.xml create mode 100644 play-services-location/core/src/main/res/values-ta/strings.xml create mode 100644 play-services-location/core/src/main/res/values-th/strings.xml create mode 100644 play-services-location/core/src/main/res/values-tr/strings.xml create mode 100644 play-services-location/core/src/main/res/values-ug/strings.xml create mode 100644 play-services-location/core/src/main/res/values-uk/strings.xml create mode 100644 play-services-location/core/src/main/res/values-vi/strings.xml create mode 100644 play-services-location/core/src/main/res/values-zh-rCN/strings.xml create mode 100644 play-services-location/core/src/main/res/values-zh-rTW/strings.xml create mode 100644 play-services-location/core/src/main/res/values/strings.xml create mode 100644 play-services-location/core/src/main/res/xml/preferences_location.xml create mode 100644 play-services-location/core/src/main/res/xml/preferences_location_all_apps.xml create mode 100644 play-services-location/core/src/main/res/xml/preferences_location_app_details.xml create mode 100644 play-services-location/core/system-api/build.gradle create mode 100644 play-services-location/core/system-api/src/main/AndroidManifest.xml create mode 100644 play-services-location/core/system-api/src/main/java/android/location/GeocoderParams.java create mode 100644 play-services-location/core/system-api/src/main/java/android/location/Geofence.java create mode 100644 play-services-location/core/system-api/src/main/java/android/location/Location.java create mode 100644 play-services-location/core/system-api/src/main/java/android/location/LocationManager.java create mode 100644 play-services-location/core/system-api/src/main/java/android/location/LocationRequest.java create mode 100644 play-services-location/core/system-api/src/main/java/android/net/wifi/WifiScanner.java create mode 100644 play-services-location/core/system-api/src/main/java/com/android/internal/location/ProviderProperties.java create mode 100644 play-services-location/core/system-api/src/main/java/com/android/internal/location/ProviderRequest.java create mode 100644 play-services-location/core/system-api/src/main/java/com/android/location/provider/GeocodeProvider.java create mode 100644 play-services-location/core/system-api/src/main/java/com/android/location/provider/LocationProvider.java create mode 100644 play-services-location/core/system-api/src/main/java/com/android/location/provider/LocationProviderBase.java create mode 100644 play-services-location/core/system-api/src/main/java/com/android/location/provider/LocationRequestUnbundled.java create mode 100644 play-services-location/core/system-api/src/main/java/com/android/location/provider/ProviderPropertiesUnbundled.java create mode 100644 play-services-location/core/system-api/src/main/java/com/android/location/provider/ProviderRequestUnbundled.java create mode 100644 play-services-location/src/main/AndroidManifest.xml 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/ActivityRecognitionResult.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/DeviceOrientation.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/GeofencingRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/GestureRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/IDeviceOrientationListener.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/ILocationCallback.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/ILocationListener.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/LocationAvailability.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/LocationRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/LocationResult.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/LocationSettingsRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/LocationSettingsResult.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/LocationStatus.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/DeviceOrientationRequestUpdateData.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/FusedLocationProviderResult.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/IBooleanStatusCallback.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/IFusedLocationProviderCallback.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGeofencerCallbacks.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGoogleLocationManagerService.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/ISettingsCallbacks.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/aidl/com/google/android/gms/location/internal/LocationRequestInternal.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/LocationRequestUpdateData.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/ParcelableGeofence.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/RemoveGeofencingRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/internal/SetGoogleLocationAccuracyRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/reporting/OptInRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/reporting/ReportingState.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/reporting/SendDataRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/reporting/UlrPrivateModeRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/reporting/UploadRequest.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/reporting/UploadRequestResult.aidl create mode 100644 play-services-location/src/main/aidl/com/google/android/gms/location/reporting/internal/IReportingService.aidl create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognition.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionApi.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionClient.java 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/ActivityRecognitionResult.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/ActivityTransitionRequest.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/DetectedActivity.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/DeviceOrientation.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/DeviceOrientationRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderApi.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderClient.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/Geofence.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/GeofenceStatusCodes.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/GeofencingApi.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/GeofencingClient.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/GeofencingEvent.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/GeofencingRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/GestureRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/Granularity.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/LocationAvailability.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/LocationCallback.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationClient.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationListener.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationResult.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationServices.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsConfiguration.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsRequest.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/LocationSettingsResult.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsStates.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsStatusCodes.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationStatus.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/LocationStatusCodes.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/Priority.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/SettingsApi.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/SettingsClient.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/SleepClassifyEvent.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/SleepSegmentEvent.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/SleepSegmentRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/ThrottleBehavior.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/DeviceOrientationRequestInternal.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/internal/DeviceOrientationRequestUpdateData.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/internal/FusedLocationProviderResult.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/internal/LocationReceiver.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/internal/LocationRequestInternal.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/internal/LocationRequestUpdateData.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/internal/ParcelableGeofence.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/internal/RemoveGeofencingRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/internal/SetGoogleLocationAccuracyRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/reporting/OptInRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/reporting/ReportingState.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/reporting/SendDataRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/reporting/UlrPrivateModeRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/reporting/UploadRequest.java create mode 100644 play-services-location/src/main/java/com/google/android/gms/location/reporting/UploadRequestResult.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionApiClientBuilder.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionApiImpl.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionClientImpl.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderApiImpl.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/GeofencingApiImpl.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/GeofencingClientImpl.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/GoogleLocationManagerClient.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/GranularityUtil.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/LocationServicesApiClientBuilder.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/PriorityUtil.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/SettingsApiImpl.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/SettingsClientImpl.java create mode 100644 play-services-location/src/main/java/org/microg/gms/location/ThrottleBehaviorUtil.java create mode 100644 play-services-maps/build.gradle create mode 100644 play-services-maps/core/hms/build.gradle create mode 100644 play-services-maps/core/hms/proguard-rules.pro create mode 100644 play-services-maps/core/hms/src/main/AndroidManifest.xml create mode 100644 play-services-maps/core/hms/src/main/assets/.gitignore create mode 100644 play-services-maps/core/hms/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/maps_dynamite/ModuleDescriptor.java create mode 100644 play-services-maps/core/hms/src/main/java/com/google/android/gms/maps/internal/CreatorImpl.java create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/CameraUpdateFactory.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/MapFragment.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/MapView.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/Projection.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/StreetViewPanoramaFragment.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/StreetViewPanoramaView.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/UiSettings.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/model/BitmapDescriptorFactory.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/model/Circle.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/model/GroundOverlay.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/model/Marker.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/model/Polygon.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/model/Polyline.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/model/TileOverlay.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/MapCompatAdapter.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/MapContext.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/MapUiController.kt create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/typeConverter.kt create mode 100644 play-services-maps/core/hms/src/main/res/drawable-hdpi/maps_default_marker.png create mode 100644 play-services-maps/core/hms/src/main/res/drawable-ldpi/maps_default_marker.png create mode 100644 play-services-maps/core/hms/src/main/res/drawable-mdpi/maps_default_marker.png create mode 100644 play-services-maps/core/hms/src/main/res/drawable-xhdpi/maps_default_marker.png create mode 100644 play-services-maps/core/hms/src/main/res/drawable-xxhdpi/maps_default_marker.png create mode 100644 play-services-maps/core/hms/src/main/res/drawable-xxxhdpi/maps_default_marker.png create mode 100644 play-services-maps/core/hms/src/main/res/raw/mapstyle_grayscale_hms.json create mode 100644 play-services-maps/core/hms/src/main/res/raw/mapstyle_night_hms.json create mode 100644 play-services-maps/core/hms/src/main/res/values/strings.xml create mode 100644 play-services-maps/core/mapbox/build.gradle create mode 100644 play-services-maps/core/mapbox/src/main/AndroidManifest.xml create mode 120000 play-services-maps/core/mapbox/src/main/assets/Open Sans Regular,Arial Unicode MS Regular create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/0-255.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/1024-1279.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/10240-10495.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/10496-10751.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/10752-11007.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/11008-11263.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/11264-11519.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/11520-11775.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/11776-12031.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/12032-12287.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/12288-12543.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/12544-12799.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/1280-1535.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/12800-13055.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/13056-13311.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/13312-13567.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/13568-13823.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/13824-14079.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/14080-14335.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/14336-14591.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/14592-14847.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/14848-15103.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/15104-15359.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/1536-1791.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/15360-15615.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/15616-15871.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/15872-16127.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/16128-16383.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/16384-16639.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/16640-16895.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/16896-17151.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/17152-17407.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/17408-17663.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/17664-17919.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/1792-2047.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/17920-18175.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/18176-18431.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/18432-18687.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/18688-18943.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/18944-19199.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/19200-19455.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/19456-19711.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/19712-19967.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/19968-20223.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/20224-20479.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/2048-2303.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/20480-20735.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/20736-20991.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/20992-21247.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/21248-21503.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/21504-21759.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/21760-22015.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/22016-22271.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/22272-22527.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/22528-22783.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/22784-23039.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/2304-2559.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/23040-23295.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/23296-23551.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/23552-23807.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/23808-24063.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/24064-24319.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/24320-24575.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/24576-24831.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/24832-25087.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/25088-25343.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/25344-25599.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/256-511.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/2560-2815.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/25600-25855.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/25856-26111.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/26112-26367.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/26368-26623.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/26624-26879.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/26880-27135.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/27136-27391.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/27392-27647.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/27648-27903.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/27904-28159.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/2816-3071.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/28160-28415.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/28416-28671.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/28672-28927.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/28928-29183.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/29184-29439.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/29440-29695.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/29696-29951.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/29952-30207.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/30208-30463.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/30464-30719.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/3072-3327.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/30720-30975.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/30976-31231.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/31232-31487.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/31488-31743.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/31744-31999.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/32000-32255.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/32256-32511.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/32512-32767.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/32768-33023.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/33024-33279.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/3328-3583.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/33280-33535.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/33536-33791.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/33792-34047.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/34048-34303.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/34304-34559.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/34560-34815.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/34816-35071.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/35072-35327.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/35328-35583.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/35584-35839.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/3584-3839.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/35840-36095.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/36096-36351.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/36352-36607.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/36608-36863.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/36864-37119.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/37120-37375.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/37376-37631.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/37632-37887.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/37888-38143.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/38144-38399.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/3840-4095.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/38400-38655.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/38656-38911.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/38912-39167.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/39168-39423.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/39424-39679.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/39680-39935.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/39936-40191.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/40192-40447.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/40448-40703.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/40704-40959.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/4096-4351.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/40960-41215.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/41216-41471.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/41472-41727.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/41728-41983.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/41984-42239.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/42240-42495.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/42496-42751.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/42752-43007.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/43008-43263.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/43264-43519.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/4352-4607.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/43520-43775.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/43776-44031.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/44032-44287.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/44288-44543.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/44544-44799.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/44800-45055.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/45056-45311.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/45312-45567.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/45568-45823.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/45824-46079.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/4608-4863.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/46080-46335.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/46336-46591.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/46592-46847.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/46848-47103.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/47104-47359.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/47360-47615.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/47616-47871.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/47872-48127.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/48128-48383.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/48384-48639.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/4864-5119.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/48640-48895.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/48896-49151.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/49152-49407.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/49408-49663.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/49664-49919.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/49920-50175.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/50176-50431.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/50432-50687.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/50688-50943.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/50944-51199.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/512-767.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/5120-5375.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/51200-51455.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/51456-51711.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/51712-51967.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/51968-52223.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/52224-52479.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/52480-52735.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/52736-52991.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/52992-53247.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/53248-53503.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/53504-53759.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/5376-5631.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/53760-54015.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/54016-54271.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/54272-54527.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/54528-54783.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/54784-55039.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/55040-55295.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/55296-55551.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/55552-55807.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/55808-56063.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/56064-56319.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/5632-5887.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/56320-56575.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/56576-56831.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/56832-57087.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/57088-57343.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/57344-57599.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/57600-57855.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/57856-58111.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/58112-58367.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/58368-58623.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/58624-58879.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/5888-6143.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/58880-59135.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/59136-59391.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/59392-59647.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/59648-59903.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/59904-60159.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/60160-60415.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/60416-60671.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/60672-60927.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/60928-61183.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/61184-61439.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/6144-6399.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/61440-61695.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/61696-61951.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/61952-62207.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/62208-62463.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/62464-62719.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/62720-62975.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/62976-63231.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/63232-63487.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/63488-63743.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/63744-63999.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/6400-6655.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/64000-64255.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/64256-64511.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/64512-64767.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/64768-65023.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/65024-65279.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/65280-65535.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/6656-6911.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/6912-7167.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/7168-7423.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/7424-7679.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/768-1023.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/7680-7935.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/7936-8191.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/8192-8447.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/8448-8703.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/8704-8959.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/8960-9215.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/9216-9471.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/9472-9727.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/9728-9983.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Bold/9984-10239.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/0-255.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/1024-1279.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/10240-10495.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/10496-10751.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/10752-11007.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/11008-11263.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/11264-11519.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/11520-11775.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/11776-12031.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/12032-12287.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/12288-12543.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/12544-12799.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/1280-1535.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/12800-13055.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/13056-13311.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/13312-13567.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/13568-13823.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/13824-14079.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/14080-14335.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/14336-14591.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/14592-14847.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/14848-15103.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/15104-15359.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/1536-1791.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/15360-15615.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/15616-15871.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/15872-16127.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/16128-16383.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/16384-16639.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/16640-16895.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/16896-17151.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/17152-17407.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/17408-17663.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/17664-17919.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/1792-2047.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/17920-18175.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/18176-18431.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/18432-18687.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/18688-18943.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/18944-19199.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/19200-19455.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/19456-19711.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/19712-19967.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/19968-20223.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/20224-20479.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/2048-2303.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/20480-20735.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/20736-20991.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/20992-21247.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/21248-21503.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/21504-21759.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/21760-22015.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/22016-22271.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/22272-22527.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/22528-22783.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/22784-23039.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/2304-2559.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/23040-23295.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/23296-23551.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/23552-23807.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/23808-24063.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/24064-24319.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/24320-24575.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/24576-24831.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/24832-25087.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/25088-25343.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/25344-25599.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/256-511.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/2560-2815.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/25600-25855.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/25856-26111.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/26112-26367.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/26368-26623.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/26624-26879.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/26880-27135.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/27136-27391.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/27392-27647.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/27648-27903.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/27904-28159.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/2816-3071.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/28160-28415.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/28416-28671.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/28672-28927.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/28928-29183.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/29184-29439.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/29440-29695.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/29696-29951.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/29952-30207.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/30208-30463.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/30464-30719.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/3072-3327.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/30720-30975.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/30976-31231.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/31232-31487.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/31488-31743.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/31744-31999.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/32000-32255.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/32256-32511.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/32512-32767.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/32768-33023.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/33024-33279.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/3328-3583.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/33280-33535.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/33536-33791.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/33792-34047.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/34048-34303.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/34304-34559.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/34560-34815.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/34816-35071.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/35072-35327.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/35328-35583.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/35584-35839.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/3584-3839.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/35840-36095.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/36096-36351.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/36352-36607.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/36608-36863.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/36864-37119.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/37120-37375.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/37376-37631.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/37632-37887.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/37888-38143.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/38144-38399.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/3840-4095.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/38400-38655.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/38656-38911.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/38912-39167.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/39168-39423.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/39424-39679.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/39680-39935.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/39936-40191.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/40192-40447.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/40448-40703.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/40704-40959.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/4096-4351.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/40960-41215.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/41216-41471.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/41472-41727.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/41728-41983.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/41984-42239.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/42240-42495.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/42496-42751.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/42752-43007.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/43008-43263.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/43264-43519.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/4352-4607.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/43520-43775.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/43776-44031.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/44032-44287.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/44288-44543.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/44544-44799.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/44800-45055.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/45056-45311.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/45312-45567.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/45568-45823.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/45824-46079.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/4608-4863.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/46080-46335.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/46336-46591.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/46592-46847.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/46848-47103.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/47104-47359.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/47360-47615.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/47616-47871.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/47872-48127.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/48128-48383.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/48384-48639.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/4864-5119.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/48640-48895.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/48896-49151.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/49152-49407.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/49408-49663.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/49664-49919.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/49920-50175.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/50176-50431.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/50432-50687.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/50688-50943.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/50944-51199.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/512-767.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/5120-5375.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/51200-51455.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/51456-51711.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/51712-51967.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/51968-52223.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/52224-52479.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/52480-52735.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/52736-52991.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/52992-53247.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/53248-53503.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/53504-53759.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/5376-5631.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/53760-54015.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/54016-54271.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/54272-54527.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/54528-54783.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/54784-55039.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/55040-55295.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/55296-55551.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/55552-55807.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/55808-56063.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/56064-56319.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/5632-5887.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/56320-56575.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/56576-56831.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/56832-57087.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/57088-57343.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/57344-57599.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/57600-57855.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/57856-58111.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/58112-58367.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/58368-58623.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/58624-58879.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/5888-6143.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/58880-59135.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/59136-59391.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/59392-59647.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/59648-59903.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/59904-60159.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/60160-60415.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/60416-60671.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/60672-60927.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/60928-61183.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/61184-61439.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/6144-6399.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/61440-61695.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/61696-61951.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/61952-62207.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/62208-62463.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/62464-62719.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/62720-62975.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/62976-63231.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/63232-63487.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/63488-63743.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/63744-63999.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/6400-6655.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/64000-64255.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/64256-64511.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/64512-64767.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/64768-65023.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/65024-65279.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/65280-65535.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/6656-6911.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/6912-7167.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/7168-7423.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/7424-7679.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/768-1023.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/7680-7935.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/7936-8191.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/8192-8447.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/8448-8703.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/8704-8959.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/8960-9215.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/9216-9471.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/9472-9727.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/9728-9983.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Medium/9984-10239.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/0-255.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/1024-1279.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/10240-10495.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/10496-10751.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/10752-11007.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/11008-11263.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/11264-11519.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/11520-11775.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/11776-12031.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/12032-12287.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/12288-12543.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/12544-12799.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/1280-1535.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/12800-13055.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/13056-13311.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/13312-13567.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/13568-13823.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/13824-14079.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/14080-14335.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/14336-14591.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/14592-14847.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/14848-15103.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/15104-15359.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/1536-1791.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/15360-15615.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/15616-15871.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/15872-16127.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/16128-16383.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/16384-16639.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/16640-16895.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/16896-17151.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/17152-17407.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/17408-17663.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/17664-17919.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/1792-2047.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/17920-18175.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/18176-18431.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/18432-18687.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/18688-18943.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/18944-19199.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/19200-19455.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/19456-19711.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/19712-19967.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/19968-20223.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/20224-20479.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/2048-2303.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/20480-20735.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/20736-20991.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/20992-21247.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/21248-21503.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/21504-21759.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/21760-22015.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/22016-22271.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/22272-22527.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/22528-22783.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/22784-23039.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/2304-2559.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/23040-23295.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/23296-23551.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/23552-23807.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/23808-24063.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/24064-24319.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/24320-24575.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/24576-24831.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/24832-25087.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/25088-25343.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/25344-25599.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/256-511.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/2560-2815.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/25600-25855.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/25856-26111.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/26112-26367.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/26368-26623.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/26624-26879.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/26880-27135.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/27136-27391.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/27392-27647.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/27648-27903.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/27904-28159.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/2816-3071.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/28160-28415.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/28416-28671.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/28672-28927.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/28928-29183.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/29184-29439.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/29440-29695.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/29696-29951.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/29952-30207.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/30208-30463.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/30464-30719.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/3072-3327.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/30720-30975.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/30976-31231.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/31232-31487.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/31488-31743.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/31744-31999.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/32000-32255.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/32256-32511.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/32512-32767.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/32768-33023.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/33024-33279.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/3328-3583.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/33280-33535.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/33536-33791.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/33792-34047.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/34048-34303.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/34304-34559.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/34560-34815.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/34816-35071.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/35072-35327.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/35328-35583.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/35584-35839.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/3584-3839.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/35840-36095.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/36096-36351.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/36352-36607.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/36608-36863.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/36864-37119.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/37120-37375.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/37376-37631.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/37632-37887.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/37888-38143.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/38144-38399.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/3840-4095.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/38400-38655.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/38656-38911.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/38912-39167.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/39168-39423.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/39424-39679.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/39680-39935.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/39936-40191.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/40192-40447.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/40448-40703.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/40704-40959.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/4096-4351.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/40960-41215.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/41216-41471.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/41472-41727.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/41728-41983.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/41984-42239.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/42240-42495.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/42496-42751.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/42752-43007.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/43008-43263.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/43264-43519.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/4352-4607.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/43520-43775.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/43776-44031.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/44032-44287.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/44288-44543.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/44544-44799.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/44800-45055.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/45056-45311.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/45312-45567.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/45568-45823.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/45824-46079.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/4608-4863.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/46080-46335.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/46336-46591.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/46592-46847.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/46848-47103.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/47104-47359.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/47360-47615.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/47616-47871.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/47872-48127.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/48128-48383.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/48384-48639.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/4864-5119.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/48640-48895.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/48896-49151.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/49152-49407.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/49408-49663.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/49664-49919.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/49920-50175.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/50176-50431.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/50432-50687.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/50688-50943.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/50944-51199.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/512-767.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/5120-5375.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/51200-51455.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/51456-51711.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/51712-51967.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/51968-52223.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/52224-52479.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/52480-52735.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/52736-52991.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/52992-53247.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/53248-53503.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/53504-53759.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/5376-5631.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/53760-54015.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/54016-54271.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/54272-54527.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/54528-54783.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/54784-55039.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/55040-55295.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/55296-55551.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/55552-55807.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/55808-56063.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/56064-56319.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/5632-5887.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/56320-56575.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/56576-56831.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/56832-57087.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/57088-57343.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/57344-57599.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/57600-57855.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/57856-58111.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/58112-58367.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/58368-58623.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/58624-58879.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/5888-6143.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/58880-59135.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/59136-59391.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/59392-59647.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/59648-59903.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/59904-60159.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/60160-60415.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/60416-60671.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/60672-60927.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/60928-61183.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/61184-61439.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/6144-6399.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/61440-61695.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/61696-61951.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/61952-62207.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/62208-62463.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/62464-62719.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/62720-62975.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/62976-63231.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/63232-63487.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/63488-63743.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/63744-63999.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/6400-6655.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/64000-64255.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/64256-64511.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/64512-64767.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/64768-65023.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/65024-65279.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/65280-65535.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/6656-6911.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/6912-7167.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/7168-7423.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/7424-7679.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/768-1023.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/7680-7935.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/7936-8191.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/8192-8447.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/8448-8703.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/8704-8959.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/8960-9215.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/9216-9471.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/9472-9727.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/9728-9983.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/Roboto Regular/9984-10239.pbf create mode 100644 play-services-maps/core/mapbox/src/main/assets/sprites.json create mode 100644 play-services-maps/core/mapbox/src/main/assets/sprites.png create mode 100644 play-services-maps/core/mapbox/src/main/assets/sprites@2x.json create mode 100644 play-services-maps/core/mapbox/src/main/assets/sprites@2x.png create mode 100644 play-services-maps/core/mapbox/src/main/assets/style-mapbox-outdoors-v12.json create mode 100644 play-services-maps/core/mapbox/src/main/assets/style-microg-normal-mapbox.json create mode 100644 play-services-maps/core/mapbox/src/main/assets/style-microg-normal-stadia.json create mode 100644 play-services-maps/core/mapbox/src/main/assets/style-microg-satellite-mapbox.json create mode 100644 play-services-maps/core/mapbox/src/main/assets/style-microg-satellite-stadia.json create mode 100644 play-services-maps/core/mapbox/src/main/assets/style-stadia-outdoors.json create mode 100644 play-services-maps/core/mapbox/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/maps_dynamite/ModuleDescriptor.java create mode 100644 play-services-maps/core/mapbox/src/main/java/com/google/android/gms/maps/internal/CreatorImpl.java create mode 100644 play-services-maps/core/mapbox/src/main/java/com/mapbox/android/accounts/v1/MapboxAccounts.java create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/AbstractGoogleMap.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/CameraBoundsWithSizeUpdate.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/CameraUpdateFactory.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleLocationEngine.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/LiteGoogleMap.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/MapFragment.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/MapView.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Pattern.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Projection.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/SourceLocationEngine.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/StreetViewPanoramaFragment.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/StreetViewPanoramaView.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Styles.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/UiSettings.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptor.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/BitmapDescriptorFactory.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Circle.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/GroundOverlay.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/InfoWindow.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Marker.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Markup.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polygon.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/Polyline.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/model/TileOverlay.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/MapContext.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/MultiArchLoader.kt create mode 100644 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/utils/typeConverter.kt create mode 100644 play-services-maps/core/mapbox/src/main/res/drawable-hdpi/maps_default_marker.png create mode 100644 play-services-maps/core/mapbox/src/main/res/drawable-ldpi/maps_default_marker.png create mode 100644 play-services-maps/core/mapbox/src/main/res/drawable-mdpi/maps_default_marker.png create mode 100644 play-services-maps/core/mapbox/src/main/res/drawable-xhdpi/maps_default_marker.png create mode 100644 play-services-maps/core/mapbox/src/main/res/drawable-xxhdpi/maps_default_marker.png create mode 100644 play-services-maps/core/mapbox/src/main/res/drawable-xxxhdpi/maps_default_marker.png create mode 100644 play-services-maps/core/mapbox/src/main/res/drawable/location_dot.xml create mode 100644 play-services-maps/core/mapbox/src/main/res/drawable/maps_default_bubble.xml create mode 100644 play-services-maps/core/mapbox/src/main/res/layout/maps_default_bubble_layout.xml create mode 100644 play-services-maps/core/vtm/build.gradle create mode 100644 play-services-maps/core/vtm/microg-theme/build.gradle create mode 100644 play-services-maps/core/vtm/microg-theme/resources/assets/styles/microg.xml create mode 100644 play-services-maps/core/vtm/microg-theme/resources/assets/symbols/dot_white.svg create mode 100644 play-services-maps/core/vtm/microg-theme/resources/assets/symbols/transit/train_station.svg create mode 100644 play-services-maps/core/vtm/microg-theme/resources/assets/symbols/transit/train_station_small.svg create mode 100644 play-services-maps/core/vtm/microg-theme/src/org/oscim/theme/MicrogThemes.java create mode 100644 play-services-maps/core/vtm/src/main/AndroidManifest.xml create mode 100644 play-services-maps/core/vtm/src/main/java/com/google/android/gms/maps/internal/CreatorImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/ApplicationContextWrapper.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/BackendMap.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/BackendMapView.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/ContainerLayout.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/GmsMapsTypeHelper.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/GoogleMapImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/MapFragmentImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/MapViewImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/ProjectionImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/ResourcesContainer.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/StreetViewPanoramaFragmentImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/StreetViewPanoramaViewImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/UiSettingsImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/bitmap/AbstractBitmapDescriptor.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/bitmap/AssetBitmapDescriptor.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/bitmap/BitmapBitmapDescriptor.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/bitmap/BitmapDescriptorFactoryImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/bitmap/BitmapDescriptorImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/bitmap/DefaultBitmapDescriptor.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/bitmap/FileBitmapDescriptor.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/bitmap/PathBitmapDescriptor.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/bitmap/ResourceBitmapDescriptor.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/camera/CameraUpdate.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/camera/CameraUpdateFactoryImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/camera/MapPositionCameraUpdate.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/camera/NoCameraUpdate.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/data/SharedTileCache.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/data/SharedTileProvider.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/CircleImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/ClearableVectorLayer.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/DrawableMarkup.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/GroundOverlayImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/InfoWindow.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/MarkerImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/MarkerItemMarkup.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/Markup.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/PolygonImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/PolylineImpl.java create mode 100644 play-services-maps/core/vtm/src/main/java/org/microg/gms/maps/vtm/markup/TileOverlayImpl.java create mode 100644 play-services-maps/core/vtm/src/main/res/drawable/maps_default_marker.png create mode 100644 play-services-maps/core/vtm/src/main/res/drawable/maps_default_window.9.png create mode 100644 play-services-maps/core/vtm/src/main/res/drawable/nop.png create mode 100644 play-services-maps/src/main/AndroidManifest.xml create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/GoogleMapOptions.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/StreetViewPanoramaOptions.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/ICameraUpdateFactoryDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/ICancelableCallback.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/ICreator.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IGoogleMapDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IInfoWindowAdapter.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/ILocationSourceDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IMapFragmentDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IMapViewDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnCameraChangeListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnCameraIdleListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnCameraMoveCanceledListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnCameraMoveListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnCameraMoveStartedListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnCircleClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnGroundOverlayClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnIndoorStateChangeListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowCloseListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnInfoWindowLongClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnLocationChangeListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMapClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMapLoadedCallback.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMapLongClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMapReadyCallback.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMarkerClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMarkerDragListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMyLocationButtonClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMyLocationChangeListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnMyLocationClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnPolygonClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnPolylineClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnStreetViewPanoramaCameraChangeListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnStreetViewPanoramaChangeListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnStreetViewPanoramaClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnStreetViewPanoramaLongClickListener.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IOnStreetViewPanoramaReadyCallback.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IProjectionDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/ISnapshotReadyCallback.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IStreetViewPanoramaDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IStreetViewPanoramaFragmentDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IStreetViewPanoramaViewDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/internal/IUiSettingsDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/CameraPosition.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Cap.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/CircleOptions.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Dash.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Dot.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Gap.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/GroundOverlayOptions.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/LatLng.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/LatLngBounds.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/MapStyleOptions.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/MarkerOptions.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/PatternItem.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/PolygonOptions.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/PolylineOptions.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/StreetViewPanoramaCamera.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/StreetViewPanoramaLocation.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/StreetViewPanoramaOrientation.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/StreetViewSource.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/StyleSpan.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/Tile.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/TileOverlayOptions.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/VisibleRegion.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IBitmapDescriptorFactoryDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ICircleDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IGroundOverlayDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IIndoorBuildingDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IIndoorLevelDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IMarkerDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IPolygonDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/IPolylineDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ITileOverlayDelegate.aidl create mode 100644 play-services-maps/src/main/aidl/com/google/android/gms/maps/model/internal/ITileProviderDelegate.aidl create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/CameraUpdate.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/CameraUpdateFactory.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/GoogleMap.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/GoogleMapOptions.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/MapFragment.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/MapView.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/MapsInitializer.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/OnMapReadyCallback.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/OnMapsSdkInitializedCallback.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/OnStreetViewPanoramaReadyCallback.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/StreetViewPanorama.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/StreetViewPanoramaFragment.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/StreetViewPanoramaOptions.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/StreetViewPanoramaView.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/BitmapDescriptor.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/BitmapDescriptorFactory.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/ButtCap.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/CameraPosition.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/Cap.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/Circle.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/CircleOptions.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/CustomCap.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/Dash.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/Dot.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/Gap.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/GroundOverlayOptions.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/JointType.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/LatLng.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/LatLngBounds.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/MapStyleOptions.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/MarkerOptions.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/PatternItem.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/PolygonOptions.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/PolylineOptions.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/RoundCap.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/RuntimeRemoteException.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/SquareCap.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/StampStyle.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/StreetViewPanoramaCamera.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/StreetViewPanoramaLink.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/StreetViewPanoramaLocation.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/StreetViewPanoramaOrientation.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/StreetViewSource.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/StrokeStyle.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/StyleSpan.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/Tile.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/TileOverlayOptions.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/TileProvider.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/VisibleRegion.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/model/package-info.java create mode 100644 play-services-maps/src/main/java/com/google/android/gms/maps/package-info.java create mode 100644 play-services-maps/src/main/java/org/microg/gms/maps/MapLifecycleDelegate.java create mode 100644 play-services-maps/src/main/java/org/microg/gms/maps/MapViewDelegate.java create mode 100644 play-services-maps/src/main/java/org/microg/gms/maps/MapViewLifecycleHelper.java create mode 100644 play-services-maps/src/main/java/org/microg/gms/maps/MapsBundleHelper.java create mode 100644 play-services-maps/src/main/java/org/microg/gms/maps/MapsContextLoader.java create mode 100644 play-services-maps/src/main/java/org/microg/gms/maps/StreetViewLifecycleDelegate.java create mode 100644 play-services-maps/src/main/java/org/microg/gms/maps/StreetViewPanoramaViewDelegate.java create mode 100644 play-services-maps/src/main/java/org/microg/gms/maps/StreetViewPanoramaViewLifecycleHelper.java create mode 100644 play-services-maps/src/main/res/values/maps_attrs.xml diff --git a/.github/workflows/crowdin_pull.yml b/.github/workflows/crowdin_pull.yml index 248ac0bd5e..71736de51e 100644 --- a/.github/workflows/crowdin_pull.yml +++ b/.github/workflows/crowdin_pull.yml @@ -60,7 +60,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | chmod +x ./gradlew - ./gradlew --no-daemon clean :play-services-core:assembleDefaultDebug + ./gradlew --no-daemon clean :play-services-core:assembleMapboxDefaultDebug # Step 3: Checkout dev branch to prepare for merging - name: Checkout dev branch for merge diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1452574d4..e7e0898ffa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,13 +47,13 @@ jobs: fi done - name: "Execute Gradle build" - run: "./gradlew --no-daemon clean :play-services-core:assembleDefaultRelease" + run: "./gradlew --no-daemon clean :play-services-core:assembleMapboxDefaultRelease" - uses: ilharp/sign-android-release@v2 name: "Sign app APK" id: sign_app with: - releaseDir: play-services-core/build/outputs/apk/default/release + releaseDir: play-services-core/build/outputs/apk/mapboxDefault/release signingKey: ${{ secrets.KEYSTORE_B64 }} keyAlias: ${{ secrets.KEYSTORE_ENTRY_ALIAS }} keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} diff --git a/build.gradle b/build.gradle index 5dc12a7323..de656294ee 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ buildscript { ext.androidBuildGradleVersion = '8.13.2' -// ext.androidBuildVersionTools = '34.0.0' + ext.androidBuildVersionTools = '35.0.0' ext.androidMinSdk = 24 ext.androidTargetSdk = 29 @@ -48,6 +48,9 @@ buildscript { ':play-services-cast-core', ':play-services-cast-framework-core', ':play-services-conscrypt-provider-core', + ':play-services-location-core', + ':play-services-location-core-base', + ':play-services-maps', ':play-services-core-proto', // deprecated ':play-services-core:microg-ui-tools', ':play-services-api', diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt index cfbfb6e2d3..70a898101a 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt @@ -95,6 +95,42 @@ object SettingsContract { ) } + object Location { + private const val id = "location" + fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), id) + fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$id" + + const val WIFI_ICHNAEA = "location_wifi_mls" + const val WIFI_MOVING = "location_wifi_moving" + const val WIFI_LEARNING = "location_wifi_learning" + const val WIFI_CACHING = "location_wifi_caching" + const val CELL_ICHNAEA = "location_cell_mls" + const val CELL_LEARNING = "location_cell_learning" + const val CELL_CACHING = "location_cell_caching" + const val GEOCODER_NOMINATIM = "location_geocoder_nominatim" + const val ICHNAEA_ENDPOINT = "location_ichnaea_endpoint" + const val ONLINE_SOURCE = "location_online_source" + const val ICHNAEA_CONTRIBUTE = "location_ichnaea_contribute" + const val MAPS_TIMELINE = "location_timeline" + const val MAPS_TIMELINE_UPLOAD = "location_timeline_upload" + + val PROJECTION = arrayOf( + WIFI_ICHNAEA, + WIFI_MOVING, + WIFI_LEARNING, + WIFI_CACHING, + CELL_ICHNAEA, + CELL_LEARNING, + CELL_CACHING, + GEOCODER_NOMINATIM, + ICHNAEA_ENDPOINT, + ONLINE_SOURCE, + ICHNAEA_CONTRIBUTE, + MAPS_TIMELINE, + MAPS_TIMELINE_UPLOAD, + ) + } + object Profile { private const val id = "profile" fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), id) diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt index 38796ea048..f8acb73dce 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt @@ -19,6 +19,7 @@ import org.microg.gms.common.PackageUtils.warnIfNotMainProcess import org.microg.gms.settings.SettingsContract.Auth import org.microg.gms.settings.SettingsContract.CheckIn import org.microg.gms.settings.SettingsContract.Gcm +import org.microg.gms.settings.SettingsContract.Location import org.microg.gms.settings.SettingsContract.Profile import org.microg.gms.settings.SettingsContract.getAuthority import java.io.File @@ -69,6 +70,7 @@ class SettingsProvider : ContentProvider() { CheckIn.getContentUri(context!!) -> queryCheckIn(projection ?: CheckIn.PROJECTION) Gcm.getContentUri(context!!) -> queryGcm(projection ?: Gcm.PROJECTION) Auth.getContentUri(context!!) -> queryAuth(projection ?: Auth.PROJECTION) + Location.getContentUri(context!!) -> queryLocation(projection ?: Location.PROJECTION) Profile.getContentUri(context!!) -> queryProfile(projection ?: Profile.PROJECTION) else -> null } @@ -85,6 +87,7 @@ class SettingsProvider : ContentProvider() { CheckIn.getContentUri(context!!) -> updateCheckIn(values) Gcm.getContentUri(context!!) -> updateGcm(values) Auth.getContentUri(context!!) -> updateAuth(values) + Location.getContentUri(context!!) -> updateLocation(values) Profile.getContentUri(context!!) -> updateProfile(values) else -> return 0 } @@ -240,6 +243,62 @@ class SettingsProvider : ContentProvider() { editor.apply() } + private fun queryLocation(p: Array): Cursor = MatrixCursor(p).addRow(p) { key -> + when (key) { + Location.WIFI_ICHNAEA -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("org.microg.nlp.backend.ichnaea")) + Location.WIFI_MOVING -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("de.sorunome.unifiednlp.trains")) + Location.WIFI_LEARNING -> getSettingsBoolean(key, false) + Location.WIFI_CACHING -> getSettingsBoolean(key, getSettingsBoolean(Location.WIFI_LEARNING, false) == 1) + Location.CELL_ICHNAEA -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("org.microg.nlp.backend.ichnaea")) + Location.CELL_LEARNING -> getSettingsBoolean(key, true) + Location.CELL_CACHING -> getSettingsBoolean(key, getSettingsBoolean(Location.CELL_LEARNING, true) == 1) + Location.GEOCODER_NOMINATIM -> getSettingsBoolean(key, hasUnifiedNlpGeocoderBackend("org.microg.nlp.backend.nominatim")) + Location.ICHNAEA_ENDPOINT -> getSettingsString(key) + Location.ONLINE_SOURCE -> getSettingsString(key) + Location.ICHNAEA_CONTRIBUTE -> getSettingsBoolean(key, false) + Location.MAPS_TIMELINE -> getSettingsBoolean(key, false) + Location.MAPS_TIMELINE_UPLOAD -> getSettingsBoolean(key, false) + else -> throw IllegalArgumentException("Unknown key: $key") + } + } + + private fun hasUnifiedNlpPrefixInStringSet(key: String, vararg prefixes: String) = + getUnifiedNlpSettingsStringSetCompat(key, emptySet()).any { entry -> + prefixes.any { prefix -> entry.startsWith(prefix) } + } + + private fun hasUnifiedNlpLocationBackend(vararg packageNames: String) = + hasUnifiedNlpPrefixInStringSet("location_backends", *packageNames.map { "$it/" }.toTypedArray()) + + private fun hasUnifiedNlpGeocoderBackend(vararg packageNames: String) = + hasUnifiedNlpPrefixInStringSet("geocoder_backends", *packageNames.map { "$it/" }.toTypedArray()) + + private fun updateLocation(values: ContentValues) { + if (values.size() == 0) return + val editor = preferences.edit() + values.valueSet().forEach { (key, value) -> + when (key) { + Location.WIFI_ICHNAEA -> editor.putBoolean(key, value as Boolean) + Location.WIFI_MOVING -> editor.putBoolean(key, value as Boolean) + Location.WIFI_LEARNING -> editor.putBoolean(key, value as Boolean) + Location.CELL_ICHNAEA -> editor.putBoolean(key, value as Boolean) + Location.CELL_LEARNING -> editor.putBoolean(key, value as Boolean) + Location.GEOCODER_NOMINATIM -> editor.putBoolean(key, value as Boolean) + Location.ICHNAEA_ENDPOINT -> (value as String).let { + if (it.isBlank()) editor.remove(key) else editor.putString(key, it) + } + Location.ONLINE_SOURCE -> (value as? String).let { + if (it.isNullOrBlank()) editor.remove(key) else editor.putString(key, it) + } + Location.ICHNAEA_CONTRIBUTE -> editor.putBoolean(key, value as Boolean) + Location.MAPS_TIMELINE -> editor.putBoolean(key, value as Boolean) + Location.MAPS_TIMELINE_UPLOAD -> editor.putBoolean(key, value as Boolean) + else -> throw IllegalArgumentException("Unknown key: $key") + } + } + editor.apply() + } + private fun MatrixCursor.addRow( p: Array, valueGetter: (String) -> Any? diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/utils/SingleInstanceUtil.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/utils/SingleInstanceUtil.kt new file mode 100644 index 0000000000..8d764cdbd9 --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/utils/SingleInstanceUtil.kt @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.utils + +private val singleInstanceLock = Any() +private val singleInstanceMap: MutableMap, Any> = hashMapOf() + +fun singleInstanceOf(tClass: Class, tCreator: () -> T): T { + val volatileItem = singleInstanceMap[tClass] + @Suppress("UNCHECKED_CAST") + if (volatileItem != null && tClass.isAssignableFrom(volatileItem.javaClass)) return volatileItem as T + val itemLock = synchronized(singleInstanceLock) { + val item = singleInstanceMap[tClass] + if (item != null) { + @Suppress("UNCHECKED_CAST") + if (tClass.isAssignableFrom(item.javaClass)) return item as T + item + } else { + val lock = Any() + singleInstanceMap[tClass] = lock + lock + } + } + synchronized(itemLock) { + val item = synchronized(singleInstanceMap) { singleInstanceMap[tClass] } + if (item == null) throw IllegalStateException() + @Suppress("UNCHECKED_CAST") + if (tClass.isAssignableFrom(item.javaClass)) return item as T + if (item != itemLock) throw IllegalStateException() + + val newItem = tCreator() + synchronized(singleInstanceMap) { + singleInstanceMap[tClass] = newItem + } + return newItem + } +} + +inline fun singleInstanceOf(noinline creator: () -> T): T = + singleInstanceOf(T::class.java, creator) diff --git a/play-services-base/core/src/main/res/drawable/ic_radio_checked.xml b/play-services-base/core/src/main/res/drawable/ic_radio_checked.xml new file mode 100644 index 0000000000..8aad32131e --- /dev/null +++ b/play-services-base/core/src/main/res/drawable/ic_radio_checked.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/play-services-base/core/src/main/res/drawable/ic_radio_unchecked.xml b/play-services-base/core/src/main/res/drawable/ic_radio_unchecked.xml new file mode 100644 index 0000000000..2b79fb0fbc --- /dev/null +++ b/play-services-base/core/src/main/res/drawable/ic_radio_unchecked.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/play-services-base/core/src/main/res/values/themes.xml b/play-services-base/core/src/main/res/values/themes.xml index 2f5b54f3cf..8bdff23559 100644 --- a/play-services-base/core/src/main/res/values/themes.xml +++ b/play-services-base/core/src/main/res/values/themes.xml @@ -3,7 +3,42 @@ ~ SPDX-License-Identifier: Apache-2.0 --> - + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/play-services-location/build.gradle b/play-services-location/build.gradle new file mode 100644 index 0000000000..9533fe5157 --- /dev/null +++ b/play-services-location/build.gradle @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2015 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + namespace "com.google.android.gms.location" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + buildFeatures { + aidl = true + } + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG implementation of play-services-location' + +dependencies { + // Dependencies from play-services-location:21.0.1 + api project(':play-services-base') + api project(':play-services-basement') + api project(':play-services-tasks') + + annotationProcessor project(':safe-parcel-processor') +} diff --git a/play-services-location/core/base/build.gradle b/play-services-location/core/base/build.gradle new file mode 100644 index 0000000000..e947b60267 --- /dev/null +++ b/play-services-location/core/base/build.gradle @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-location') + implementation project(':play-services-base-core') +} + +android { + namespace "org.microg.gms.location.base" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + def onlineSourcesString = "" + if (localProperties.get("location.online-sources", "") != "") { + onlineSourcesString = localProperties.get("location.online-sources", "[]") + } else if (localProperties.get("ichnaea.endpoint", "") != "") { + onlineSourcesString = "[{\"id\": \"default\", \"url\": \"${localProperties.get("ichnaea.endpoint", "")}\"},{\"id\": \"custom\", \"import\": true}]" + } else { + onlineSourcesString = "[{\"id\": \"beacondb\", \"name\": \"BeaconDB\", \"url\": \"https://api.beacondb.net/\", \"host\": \"beacondb.net\", \"terms\": \"https://beacondb.net/privacy/\", \"import\": true, \"allowContribute\": true},{\"id\": \"custom\", \"import\": true}]" + } + buildConfigField "java.util.List", "ONLINE_SOURCES", "org.microg.gms.location.network.OnlineSourceKt.parseOnlineSources(\"${onlineSourcesString.replaceAll("\"", "\\\\\"")}\")" + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_18 + targetCompatibility = JavaVersion.VERSION_18 + } + + kotlinOptions { + jvmTarget = '18' + } +} diff --git a/play-services-location/core/base/src/main/AndroidManifest.xml b/play-services-location/core/base/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..115884d0b0 --- /dev/null +++ b/play-services-location/core/base/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + diff --git a/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt new file mode 100644 index 0000000000..13ad724c9a --- /dev/null +++ b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/LocationSettings.kt @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import org.microg.gms.location.base.BuildConfig +import org.microg.gms.settings.SettingsContract + +private const val PATH_GEOLOCATE = "/v1/geolocate" +private const val PATH_GEOLOCATE_QUERY = "/v1/geolocate?" +private const val PATH_GEOSUBMIT = "/v2/geosubmit" +private const val PATH_GEOSUBMIT_QUERY = "/v2/geosubmit?" +private const val PATH_QUERY_ONLY = "/?" + +class LocationSettings(private val context: Context) { + private fun getSettings(vararg projection: String, f: (Cursor) -> T): T = + SettingsContract.getSettings(context, SettingsContract.Location.getContentUri(context), projection, f) + + private fun setSettings(v: ContentValues.() -> Unit) = SettingsContract.setSettings(context, SettingsContract.Location.getContentUri(context), v) + + var wifiIchnaea: Boolean + get() = getSettings(SettingsContract.Location.WIFI_ICHNAEA) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.WIFI_ICHNAEA, value) } + + var wifiMoving: Boolean + get() = getSettings(SettingsContract.Location.WIFI_MOVING) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.WIFI_MOVING, value) } + + var wifiLearning: Boolean + get() = getSettings(SettingsContract.Location.WIFI_LEARNING) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.WIFI_LEARNING, value) } + + var wifiCaching: Boolean + get() = getSettings(SettingsContract.Location.WIFI_CACHING) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.WIFI_CACHING, value) } + + var cellIchnaea: Boolean + get() = getSettings(SettingsContract.Location.CELL_ICHNAEA) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.CELL_ICHNAEA, value) } + + var cellLearning: Boolean + get() = getSettings(SettingsContract.Location.CELL_LEARNING) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.CELL_LEARNING, value) } + + var cellCaching: Boolean + get() = getSettings(SettingsContract.Location.CELL_CACHING) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.CELL_CACHING, value) } + + var geocoderNominatim: Boolean + get() = getSettings(SettingsContract.Location.GEOCODER_NOMINATIM) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.GEOCODER_NOMINATIM, value) } + + var customEndpoint: String? + get() { + try { + var endpoint = getSettings(SettingsContract.Location.ICHNAEA_ENDPOINT) { c -> c.getString(0) } + // This is only temporary as users might have already broken configuration. + // Usually this would be corrected before storing it in settings, see below. + if (endpoint.endsWith(PATH_GEOLOCATE)) { + endpoint = endpoint.substring(0, endpoint.length - PATH_GEOLOCATE.length + 1) + } else if (endpoint.contains(PATH_GEOLOCATE_QUERY)) { + endpoint = endpoint.replace(PATH_GEOLOCATE_QUERY, PATH_QUERY_ONLY) + } else if (endpoint.endsWith(PATH_GEOSUBMIT)) { + endpoint = endpoint.substring(0, endpoint.length - PATH_GEOSUBMIT.length + 1) + } else if (endpoint.contains(PATH_GEOSUBMIT_QUERY)) { + endpoint = endpoint.replace(PATH_GEOSUBMIT_QUERY, PATH_QUERY_ONLY) + } + return endpoint + } catch (e: Exception) { + return null + } + } + set(value) { + val endpoint = if (value == null) { + null + } else if (value.endsWith(PATH_GEOLOCATE)) { + value.substring(0, value.length - PATH_GEOLOCATE.length + 1) + } else if (value.contains(PATH_GEOLOCATE_QUERY)) { + value.replace(PATH_GEOLOCATE_QUERY, PATH_QUERY_ONLY) + } else if (value.endsWith(PATH_GEOSUBMIT)) { + value.substring(0, value.length - PATH_GEOSUBMIT.length + 1) + } else if (value.contains(PATH_GEOSUBMIT_QUERY)) { + value.replace(PATH_GEOSUBMIT_QUERY, PATH_QUERY_ONLY) + } else { + value + } + setSettings { put(SettingsContract.Location.ICHNAEA_ENDPOINT, endpoint) } + } + + var onlineSourceId: String? + get() = getSettings(SettingsContract.Location.ONLINE_SOURCE) { c -> c.getString(0) } + set(value) = setSettings { put(SettingsContract.Location.ONLINE_SOURCE, value) } + + var ichnaeaContribute: Boolean + get() = getSettings(SettingsContract.Location.ICHNAEA_CONTRIBUTE) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.ICHNAEA_CONTRIBUTE, value) } + + var mapsTimeline: Boolean + get() = getSettings(SettingsContract.Location.MAPS_TIMELINE) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.MAPS_TIMELINE, value) } + + var mapsTimelineUpload: Boolean + get() = getSettings(SettingsContract.Location.MAPS_TIMELINE_UPLOAD) { c -> c.getInt(0) != 0 } + set(value) = setSettings { put(SettingsContract.Location.MAPS_TIMELINE_UPLOAD, value) } +} \ No newline at end of file diff --git a/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/extensions.kt b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/extensions.kt new file mode 100644 index 0000000000..9abf99f5f9 --- /dev/null +++ b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/extensions.kt @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.location.Location +import android.os.SystemClock +import android.text.format.DateUtils +import androidx.core.location.LocationCompat + +const val ACTION_NETWORK_LOCATION_SERVICE = "org.microg.gms.location.network.ACTION_NETWORK_LOCATION_SERVICE" +const val EXTRA_LOCATION = "location" +const val EXTRA_ELAPSED_REALTIME = "elapsed_realtime" +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 ACTION_CONFIGURATION_REQUIRED = "org.microg.gms.location.network.ACTION_CONFIGURATION_REQUIRED" +const val EXTRA_CONFIGURATION = "config" +const val CONFIGURATION_FIELD_ONLINE_SOURCE = "online_source" + +const val ACTION_NETWORK_IMPORT_EXPORT = "org.microg.gms.location.network.ACTION_NETWORK_IMPORT_EXPORT" +const val EXTRA_DIRECTION = "direction" +const val DIRECTION_IMPORT = "import" +const val DIRECTION_EXPORT = "export" +const val EXTRA_NAME = "name" +const val NAME_WIFI = "wifi" +const val NAME_CELL = "cell" +const val EXTRA_URI = "uri" +const val EXTRA_MESSENGER = "messenger" +const val EXTRA_REPLY_WHAT = "what" + +val Location.elapsedMillis: Long + get() = LocationCompat.getElapsedRealtimeMillis(this) + +fun Long.formatRealtime(): CharSequence = if (this <= 0) "n/a" else DateUtils.getRelativeTimeSpanString((this - SystemClock.elapsedRealtime()) + System.currentTimeMillis(), System.currentTimeMillis(), 0) +fun Long.formatDuration(): CharSequence { + if (this == 0L) return "0ms" + if (this > 315360000000L /* ten years */) return "\u221e" + val interval = listOf(1000, 60, 60, 24, Long.MAX_VALUE) + val intervalName = listOf("ms", "s", "m", "h", "d") + var ret = "" + var rem = this + for (i in 0 until interval.size) { + val mod = rem % interval[i] + if (mod != 0L) { + ret = "$mod${intervalName[i]}$ret" + } + rem /= interval[i] + if (mod == 0L && rem == 1L) { + ret = "${interval[i]}${intervalName[i]}$ret" + break + } else if (rem == 0L) { + break + } + } + return ret +} + +private var hasNetworkLocationServiceBuiltInFlag: Boolean? = null +fun Context.hasNetworkLocationServiceBuiltIn(): Boolean { + var flag = hasNetworkLocationServiceBuiltInFlag + if (flag == null) { + try { + val serviceIntent = Intent().apply { + action = ACTION_NETWORK_LOCATION_SERVICE + setPackage(packageName) + } + val services = packageManager?.queryIntentServices(serviceIntent, PackageManager.MATCH_DEFAULT_ONLY) + flag = services?.isNotEmpty() ?: false + hasNetworkLocationServiceBuiltInFlag = flag + return flag + } catch (e: Exception) { + hasNetworkLocationServiceBuiltInFlag = false + return false + } + } else { + return flag + } +} \ No newline at end of file diff --git a/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/network/OnlineSource.kt b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/network/OnlineSource.kt new file mode 100644 index 0000000000..613cf57837 --- /dev/null +++ b/play-services-location/core/base/src/main/kotlin/org/microg/gms/location/network/OnlineSource.kt @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network + +import android.net.Uri +import android.util.Log +import org.json.JSONArray +import org.json.JSONObject +import org.microg.gms.location.LocationSettings +import org.microg.gms.location.base.BuildConfig + +fun parseOnlineSources(string: String): List = JSONArray(string).let { array -> + (0 until array.length()).map { parseOnlineSource(array.getJSONObject(it)) }.also { Log.d("Location", "parseOnlineSources: ${it.joinToString()}") } +} + +fun parseOnlineSource(json: JSONObject): OnlineSource { + val id = json.getString("id") + val url = json.optString("url").takeIf { it.isNotBlank() } + val host = json.optString("host").takeIf { it.isNotBlank() } ?: runCatching { Uri.parse(url).host }.getOrNull() + val name = json.optString("name").takeIf { it.isNotBlank() } ?: host + return OnlineSource( + id = id, + name = name, + url = url, + host = host, + terms = json.optString("terms").takeIf { it.isNotBlank() }?.let { runCatching { Uri.parse(it) }.getOrNull() }, + suggested = json.optBoolean("suggested", false), + import = json.optBoolean("import", false), + allowContribute = json.optBoolean("allowContribute", false), + ) +} + +data class OnlineSource( + val id: String, + val name: String? = null, + val url: String? = null, + val host: String? = null, + val terms: Uri? = null, + /** + * Show suggested flag + */ + val suggested: Boolean = false, + /** + * If set, automatically import from custom URL if host matches (is the same domain suffix) + */ + val import: Boolean = false, + val allowContribute: Boolean = false, +) { + companion object { + /** + * Entry to allow configuring a custom URL + */ + val ID_CUSTOM = "custom" + + /** + * Legacy compatibility + */ + val ID_DEFAULT = "default" + + val ALL: List = BuildConfig.ONLINE_SOURCES + } +} + +val LocationSettings.onlineSource: OnlineSource? + get() { + val id = onlineSourceId + if (id != null) { + val source = OnlineSource.ALL.firstOrNull { it.id == id } + if (source != null) return source + } + val endpoint = customEndpoint + if (endpoint != null) { + val endpointHostSuffix = runCatching { "." + Uri.parse(endpoint).host }.getOrNull() + if (endpointHostSuffix != null) { + for (source in OnlineSource.ALL) { + if (source.import && endpointHostSuffix.endsWith("." + source.host)) { + return source + } + } + } + val customSource = OnlineSource.ALL.firstOrNull { it.id == OnlineSource.ID_CUSTOM } + if (customSource != null && customSource.import) { + return customSource + } + } + if (OnlineSource.ALL.size == 1) return OnlineSource.ALL.single() + return OnlineSource.ALL.firstOrNull { it.id == OnlineSource.ID_DEFAULT } + } + +val LocationSettings.effectiveEndpoint: String? + get() { + val source = onlineSource ?: return null + if (source.id == OnlineSource.ID_CUSTOM) return customEndpoint + return source.url + } \ No newline at end of file diff --git a/play-services-location/core/build.gradle b/play-services-location/core/build.gradle new file mode 100644 index 0000000000..734fc13596 --- /dev/null +++ b/play-services-location/core/build.gradle @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-location') + implementation project(':play-services-base-core') + implementation project(':play-services-location-core-base') + + implementation "androidx.core:core-ktx:$coreVersion" + implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" + implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" + implementation "androidx.preference:preference-ktx:$preferenceVersion" + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" + + implementation "com.android.volley:volley:$volleyVersion" + + compileOnly project(':play-services-maps') +} + +android { + namespace "org.microg.gms.location.core" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + buildConfigField "String", "FORCE_SHOW_BACKGROUND_PERMISSION", "\"\"" + buildConfigField "boolean", "SHOW_NOTIFICATION_WHEN_NOT_PERMITTED", "false" + } + + lintOptions { + disable 'MissingTranslation', 'GetLocales' + } + + flavorDimensions = ['target'] + productFlavors { + "default" { + dimension 'target' + } + "huawei" { + dimension 'target' + buildConfigField "String", "FORCE_SHOW_BACKGROUND_PERMISSION", "\"com.huawei.permission.sec.MDM.v2\"" + buildConfigField "boolean", "SHOW_NOTIFICATION_WHEN_NOT_PERMITTED", "true" + } + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + huawei.java.srcDirs += 'src/huawei/kotlin' + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_18 + targetCompatibility = JavaVersion.VERSION_18 + } + + kotlinOptions { + jvmTarget = '18' + } +} diff --git a/play-services-location/core/provider/build.gradle b/play-services-location/core/provider/build.gradle new file mode 100644 index 0000000000..29a3cf9fd7 --- /dev/null +++ b/play-services-location/core/provider/build.gradle @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-location') + compileOnly project(':play-services-location-core-system-api') + implementation project(':play-services-base-core') + implementation project(':play-services-location-core-base') + + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation "androidx.core:core-ktx:$coreVersion" + implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" + + implementation "com.android.volley:volley:$volleyVersion" + + implementation 'org.microg:address-formatter:0.3.1' +} + +android { + namespace "org.microg.gms.location.provider" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + buildConfigField "String", "ICHNAEA_USER_AGENT", "\"microG/${version}\"" + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_18 + targetCompatibility = JavaVersion.VERSION_18 + } + + kotlinOptions { + jvmTarget = '18' + } +} diff --git a/play-services-location/core/provider/src/main/AndroidManifest.xml b/play-services-location/core/provider/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..ff8cc6dd32 --- /dev/null +++ b/play-services-location/core/provider/src/main/AndroidManifest.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/DatabaseExportFileProvider.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/DatabaseExportFileProvider.kt new file mode 100644 index 0000000000..7a220a613a --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/DatabaseExportFileProvider.kt @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network + +import android.net.Uri +import androidx.core.content.FileProvider + +class DatabaseExportFileProvider : FileProvider() { + override fun getType(uri: Uri): String? { + try { + if (uri.lastPathSegment?.startsWith("cell-") == true) { + return "application/vnd.microg.location.cell+csv+gzip" + } + if (uri.lastPathSegment?.startsWith("wifi-") == true) { + return "application/vnd.microg.location.wifi+csv+gzip" + } + } catch (ignored: Exception) {} + return super.getType(uri) + } +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationDatabase.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationDatabase.kt new file mode 100644 index 0000000000..84861280fc --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/LocationDatabase.kt @@ -0,0 +1,674 @@ +/* + * 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.DatabaseUtils +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteException +import android.database.sqlite.SQLiteOpenHelper +import android.location.Location +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.util.Log +import androidx.core.content.FileProvider +import androidx.core.content.contentValuesOf +import androidx.core.database.getDoubleOrNull +import androidx.core.database.getStringOrNull +import androidx.core.os.BundleCompat +import org.microg.gms.location.NAME_CELL +import org.microg.gms.location.NAME_WIFI +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.location.network.wifi.isRequestable +import org.microg.gms.location.network.wifi.macBytes +import org.microg.gms.utils.toHexString +import java.io.File +import java.io.FileNotFoundException +import java.io.PrintWriter +import java.util.* +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +private const val CURRENT_VERSION = 8 + +internal class LocationDatabase(private val context: Context) : SQLiteOpenHelper(context, "geocache.db", null, CURRENT_VERSION) { + private data class Migration(val apply: String?, val revert: String?, val allowApplyFailure: Boolean, val allowRevertFailure: Boolean) + private val migrations: Map> + + init { + val migrations = mutableMapOf>() + fun declare(version: Int, apply: String, revert: String? = null, allowFailure: Boolean = false, allowApplyFailure: Boolean = allowFailure, allowRevertFailure: Boolean = allowFailure) { + if (!migrations.containsKey(version)) + migrations[version] = arrayListOf() + migrations[version]!!.add(Migration(apply, revert, allowApplyFailure, allowRevertFailure)) + } + + declare(3, "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);") + declare(3, "CREATE TABLE $TABLE_CELLS_PRE($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TIME INTEGER NOT NULL);") + declare(3, "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);") + declare(3, "CREATE TABLE $TABLE_CELLS_LEARN($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_HIGH REAL NOT NULL, $FIELD_LATITUDE_LOW REAL NOT NULL, $FIELD_LONGITUDE_HIGH REAL NOT NULL, $FIELD_LONGITUDE_LOW REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_BAD_TIME INTEGER);") + declare(3, "CREATE TABLE $TABLE_WIFI_LEARN($FIELD_MAC BLOB, $FIELD_LATITUDE_HIGH REAL NOT NULL, $FIELD_LATITUDE_LOW REAL NOT NULL, $FIELD_LONGITUDE_HIGH REAL NOT NULL, $FIELD_LONGITUDE_LOW REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_BAD_TIME INTEGER);") + declare(3, "CREATE UNIQUE INDEX ${TABLE_CELLS}_index ON $TABLE_CELLS($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);") + declare(3, "CREATE UNIQUE INDEX ${TABLE_CELLS_PRE}_index ON $TABLE_CELLS_PRE($FIELD_MCC, $FIELD_MNC);") + declare(3, "CREATE UNIQUE INDEX ${TABLE_WIFIS}_index ON $TABLE_WIFIS($FIELD_MAC);") + declare(3, "CREATE UNIQUE INDEX ${TABLE_CELLS_LEARN}_index ON $TABLE_CELLS_LEARN($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);") + declare(3, "CREATE UNIQUE INDEX ${TABLE_WIFI_LEARN}_index ON $TABLE_WIFI_LEARN($FIELD_MAC);") + declare(3, "CREATE INDEX ${TABLE_CELLS}_time_index ON $TABLE_CELLS($FIELD_TIME);") + declare(3, "CREATE INDEX ${TABLE_CELLS_PRE}_time_index ON $TABLE_CELLS_PRE($FIELD_TIME);") + declare(3, "CREATE INDEX ${TABLE_WIFIS}_time_index ON $TABLE_WIFIS($FIELD_TIME);") + declare(3, "CREATE INDEX ${TABLE_CELLS_LEARN}_time_index ON $TABLE_CELLS_LEARN($FIELD_TIME);") + declare(3, "CREATE INDEX ${TABLE_WIFI_LEARN}_time_index ON $TABLE_WIFI_LEARN($FIELD_TIME);") + declare(3, "DROP TABLE IF EXISTS $TABLE_WIFI_SCANS;", allowFailure = true) + + declare(4, "ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_LEARN_RECORD_COUNT INTEGER;") + declare(4, "ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_LEARN_RECORD_COUNT INTEGER;") + + declare(5, "ALTER TABLE $TABLE_CELLS ADD COLUMN $FIELD_ALTITUDE REAL;") + declare(5, "ALTER TABLE $TABLE_CELLS ADD COLUMN $FIELD_ALTITUDE_ACCURACY REAL;") + declare(5, "ALTER TABLE $TABLE_WIFIS ADD COLUMN $FIELD_ALTITUDE REAL;") + declare(5, "ALTER TABLE $TABLE_WIFIS ADD COLUMN $FIELD_ALTITUDE_ACCURACY REAL;") + + declare(6, "ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_ALTITUDE_HIGH REAL;") + declare(6, "ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_ALTITUDE_LOW REAL;") + declare(6, "ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_ALTITUDE_HIGH REAL;") + declare(6, "ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_ALTITUDE_LOW REAL;") + + declare(8, "DELETE FROM $TABLE_WIFIS WHERE $FIELD_ACCURACY = 0.0 AND ($FIELD_LATITUDE != 0.0 OR $FIELD_LONGITUDE != 0.0);", allowRevertFailure = true) + declare(8, "DELETE FROM $TABLE_CELLS WHERE $FIELD_ACCURACY = 0.0 AND ($FIELD_LATITUDE != 0.0 OR $FIELD_LONGITUDE != 0.0);", allowRevertFailure = true) + declare(8, "UPDATE $TABLE_CELLS_LEARN SET $FIELD_ALTITUDE_LOW = NULL WHERE $FIELD_ALTITUDE_LOW = 0.0;", allowRevertFailure = true) + declare(8, "UPDATE $TABLE_CELLS_LEARN SET $FIELD_ALTITUDE_HIGH = NULL WHERE $FIELD_ALTITUDE_HIGH = 0.0;", allowRevertFailure = true) + declare(8, "UPDATE $TABLE_WIFI_LEARN SET $FIELD_ALTITUDE_LOW = NULL WHERE $FIELD_ALTITUDE_LOW = 0.0;", allowRevertFailure = true) + declare(8, "UPDATE $TABLE_WIFI_LEARN SET $FIELD_ALTITUDE_HIGH = NULL WHERE $FIELD_ALTITUDE_HIGH = 0.0;", allowRevertFailure = true) + + this.migrations = migrations + } + + private fun migrate(db: SQLiteDatabase, oldVersion: Int, newVersion: Int, allowFailure: Boolean = false) { + var currentVersion = oldVersion + while (currentVersion < newVersion) { + val nextVersion = currentVersion + 1 + val migrations = this.migrations[nextVersion].orEmpty() + for (migration in migrations) { + if (migration.apply == null && !migration.allowApplyFailure && !allowFailure) + throw SQLiteException("Incomplete migration from $currentVersion to $nextVersion") + try { + db.execSQL(migration.apply) + Log.d(TAG, "Applied migration from version $currentVersion to $nextVersion: ${migration.apply}") + } catch (e: Exception) { + Log.w(TAG, "Error while applying migration from version $currentVersion to $nextVersion: ${migration.apply}", e) + if (!migration.allowApplyFailure && !allowFailure) + throw e + } + } + + currentVersion = nextVersion + } + while (currentVersion > newVersion) { + val nextVersion = currentVersion - 1 + val migrations = this.migrations[currentVersion].orEmpty() + for (migration in migrations.asReversed()) { + if (migration.revert == null && !migration.allowRevertFailure && !allowFailure) + throw SQLiteException("Incomplete migration from $currentVersion to $nextVersion") + try { + db.execSQL(migration.revert) + Log.d(TAG, "Reverted migration from version $currentVersion to $nextVersion: ${migration.revert}") + } catch (e: Exception) { + Log.w(TAG, "Error while reverting migration from version $currentVersion to $nextVersion: ${migration.revert}", e) + if (!migration.allowRevertFailure && !allowFailure) + throw e + } + } + + currentVersion = nextVersion + } + Log.i(TAG, "Migrated from $oldVersion to $newVersion") + } + + private fun SQLiteDatabase.query(table: String, columns: Array, selection: String? = null, selectionArgs: Array? = null) = + query(table, columns, selection, selectionArgs, null, null, null) + + fun getCellLocation(cell: CellDetails, allowLearned: Boolean = true): Location? { + var cursor = readableDatabase.query(TABLE_CELLS, FIELDS_CACHE_LOCATION, CELLS_SELECTION, getCellSelectionArgs(cell)) + val cellLocation = cursor.getSingleLocation(MAX_CACHE_AGE) + if (allowLearned) { + cursor = readableDatabase.query(TABLE_CELLS_LEARN, FIELDS_MID_LOCATION_GET_LEARN, CELLS_SELECTION, getCellSelectionArgs(cell)) + try { + if (cursor.moveToNext()) { + val badTime = cursor.getLong(8) + val time = cursor.getLong(7) + if (badTime < time - LEARN_BAD_CUTOFF) { + cursor.getCellMidLocation()?.let { + if (cellLocation == null || cellLocation == NEGATIVE_CACHE_ENTRY || cellLocation.precision < it.precision) return it + } + } + } + } finally { + cursor.close() + } + } + if (cellLocation != null) return cellLocation + cursor = readableDatabase.query(TABLE_CELLS_PRE, arrayOf(FIELD_TIME), CELLS_PRE_SELECTION, getCellPreSelectionArgs(cell)) + try { + if (cursor.moveToNext()) { + if (cursor.getLong(1) > System.currentTimeMillis() - MAX_CACHE_AGE) { + return NEGATIVE_CACHE_ENTRY + } + } + } finally { + cursor.close() + } + return null + } + + fun getWifiLocation(wifi: WifiDetails, allowLearned: Boolean = true): Location? { + var cursor = readableDatabase.query(TABLE_WIFIS, FIELDS_CACHE_LOCATION, getWifiSelection(wifi)) + val wifiLocation = cursor.getSingleLocation(MAX_CACHE_AGE) + if (allowLearned) { + cursor = readableDatabase.query(TABLE_WIFI_LEARN, FIELDS_MID_LOCATION_GET_LEARN, getWifiSelection(wifi)) + try { + if (cursor.moveToNext()) { + val badTime = cursor.getLong(8) + val time = cursor.getLong(7) + if (badTime < time - LEARN_BAD_CUTOFF) { + cursor.getWifiMidLocation()?.let { + if (wifiLocation == null || wifiLocation == NEGATIVE_CACHE_ENTRY || wifiLocation.precision < it.precision) return it + } + } + } + } finally { + cursor.close() + } + } + return wifiLocation + } + + fun putCellLocation(cell: CellDetails, location: Location) { + if (!cell.isValid) return + val cv = contentValuesOf( + FIELD_MCC to cell.mcc, + FIELD_MNC to cell.mnc, + FIELD_LAC_TAC to (cell.lac ?: cell.tac ?: 0), + FIELD_TYPE to cell.type.ordinal, + FIELD_CID to cell.cid, + FIELD_PSC to (cell.pscOrPci ?: 0) + ).apply { putLocation(location) } + writableDatabase.insertWithOnConflict(TABLE_CELLS, null, cv, SQLiteDatabase.CONFLICT_REPLACE) + } + + fun putWifiLocation(wifi: WifiDetails, location: Location) { + if (!wifi.isRequestable) return + val cv = contentValuesOf( + FIELD_MAC to wifi.macBytes + ).apply { putLocation(location) } + writableDatabase.insertWithOnConflict(TABLE_WIFIS, null, cv, SQLiteDatabase.CONFLICT_REPLACE) + } + + fun learnCellLocation(cell: CellDetails, location: Location, import: Boolean = false): Boolean { + if (!cell.isValid) return false + val cursor = readableDatabase.query(TABLE_CELLS_LEARN, FIELDS_MID_LOCATION, CELLS_SELECTION, getCellSelectionArgs(cell)) + var exists = false + var isBad = false + var midLocation: Location? = null + try { + if (cursor.moveToNext()) { + midLocation = cursor.getMidLocation() + exists = midLocation != null + isBad = midLocation?.let { it.distanceTo(location) > LEARN_BAD_SIZE_CELL } == true + } + } finally { + cursor.close() + } + if (exists && isBad) { + val values = ContentValues().apply { putLearnLocation(location, badTime = location.time, import = import) } + writableDatabase.update(TABLE_CELLS_LEARN, values, CELLS_SELECTION, getCellSelectionArgs(cell)) + } else if (!exists) { + val values = contentValuesOf( + FIELD_MCC to cell.mcc, + FIELD_MNC to cell.mnc, + FIELD_LAC_TAC to (cell.lac ?: cell.tac ?: 0), + FIELD_TYPE to cell.type.ordinal, + FIELD_CID to cell.cid, + FIELD_PSC to (cell.pscOrPci ?: 0), + ).apply { putLearnLocation(location, badTime = 0) } + writableDatabase.insertWithOnConflict(TABLE_CELLS_LEARN, null, values, SQLiteDatabase.CONFLICT_REPLACE) + } else { + val values = ContentValues().apply { putLearnLocation(location, midLocation) } + writableDatabase.update(TABLE_CELLS_LEARN, values, CELLS_SELECTION, getCellSelectionArgs(cell)) + } + return true + } + + fun learnWifiLocation(wifi: WifiDetails, location: Location, import: Boolean = false): Boolean { + if (!wifi.isRequestable) return false + val cursor = readableDatabase.query(TABLE_WIFI_LEARN, FIELDS_MID_LOCATION, getWifiSelection(wifi)) + var exists = false + var isBad = false + var midLocation: Location? = null + try { + if (cursor.moveToNext()) { + midLocation = cursor.getMidLocation() + exists = midLocation != null + isBad = midLocation?.let { it.distanceTo(location) > LEARN_BAD_SIZE_WIFI } == true + } + } finally { + cursor.close() + } + if (exists && isBad) { + val values = ContentValues().apply { putLearnLocation(location, badTime = location.time, import = import) } + writableDatabase.update(TABLE_WIFI_LEARN, values, getWifiSelection(wifi), null) + } else if (!exists) { + val values = contentValuesOf( + FIELD_MAC to wifi.macBytes + ).apply { putLearnLocation(location, badTime = 0) } + writableDatabase.insertWithOnConflict(TABLE_WIFI_LEARN, null, values, SQLiteDatabase.CONFLICT_REPLACE) + } else { + val values = ContentValues().apply { putLearnLocation(location, midLocation) } + writableDatabase.update(TABLE_WIFI_LEARN, values, getWifiSelection(wifi), null) + } + return true + } + + fun exportLearned(name: String): Uri? { + try { + val wifi = when (name) { + NAME_WIFI -> true + NAME_CELL -> false + else -> throw IllegalArgumentException() + } + val fieldNames = if (wifi) FIELDS_WIFI else FIELDS_CELL + val tableName = if (wifi) TABLE_WIFI_LEARN else TABLE_CELLS_LEARN + val midLocationGetter: (Cursor) -> Location? = if (wifi) Cursor::getWifiMidLocation else Cursor::getCellMidLocation + + val exportDir = File(context.cacheDir, "location") + exportDir.mkdir() + val exportFile = File(exportDir, "$name-${UUID.randomUUID()}.csv.gz") + val output = GZIPOutputStream(exportFile.outputStream()).bufferedWriter() + output.write("${fieldNames.joinToString(",")},${FIELDS_EXPORT_DATA.joinToString(",")}\n") + val cursor = readableDatabase.query(tableName, FIELDS_MID_LOCATION + fieldNames) + val indices = fieldNames.map { cursor.getColumnIndexOrThrow(it) } + while (cursor.moveToNext()) { + val midLocation = midLocationGetter(cursor) + if (midLocation != null) { + output.write(indices.joinToString(",") { index -> + if (cursor.getType(index) == Cursor.FIELD_TYPE_BLOB) { + cursor.getBlob(index).toHexString() + } else { + cursor.getStringOrNull(index) ?: "" + } + }) + output.write(",${midLocation.latitude},${midLocation.longitude},${if (midLocation.hasAltitude()) midLocation.altitude else ""}\n") + } + } + output.close() + return FileProvider.getUriForFile(context,"${context.packageName}.microg.location.export", exportFile) + } catch (e: Exception) { + Log.w(TAG, e) + } + return null + } + + fun importLearned(fileUri: Uri): Int { + var counter = 0 + try { + val type = context.contentResolver.getType(fileUri) + val gzip = if (type == null || type !in SUPPORTED_TYPES) { + if (fileUri.path == null) throw IllegalArgumentException("Unsupported file extension") + if (fileUri.path!!.endsWith(".gz")) { + true + } else if (fileUri.path!!.endsWith(".csv")) { + false + } else { + throw IllegalArgumentException("Unsupported file extension") + } + } else { + type.endsWith("gzip") + } + val desc = context.contentResolver.openFileDescriptor(fileUri, "r") ?: throw FileNotFoundException() + ParcelFileDescriptor.AutoCloseInputStream(desc).use { source -> + val input = (if (gzip) GZIPInputStream(source) else source).bufferedReader() + val headers = input.readLine().split(",") + val name = when { + headers.containsAll(FIELDS_WIFI.toList()) && headers.containsAll(FIELDS_EXPORT_DATA.toList()) -> NAME_WIFI + headers.containsAll(FIELDS_CELL.toList()) && headers.containsAll(FIELDS_EXPORT_DATA.toList()) -> NAME_CELL + else -> null + } + if (name != null) { + while (true) { + val line = input.readLine().split(",") + if (line.size != headers.size) break // End of file reached + val location = Location(PROVIDER_CACHE) + location.latitude = line[headers.indexOf(FIELD_LATITUDE)].toDoubleOrNull() ?: continue + location.longitude = line[headers.indexOf(FIELD_LONGITUDE)].toDoubleOrNull() ?: continue + line[headers.indexOf(FIELD_ALTITUDE)].toDoubleOrNull()?.let { location.altitude = it } + location.time = headers.indexOf(FIELD_TIME).takeIf { it != -1 }?.let { line[it] }?.toLongOrNull()?.takeIf { it > 0 } ?: System.currentTimeMillis() + if (name == NAME_WIFI) { + val wifi = WifiDetails( + macAddress = line[headers.indexOf(FIELD_MAC)] + ) + if (learnWifiLocation(wifi, location)) counter++ + } else { + val cell = CellDetails( + type = line[headers.indexOf(FIELD_TYPE)].let { + it.toIntOrNull()?.let { CellDetails.Companion.Type.entries[it] } ?: + runCatching { CellDetails.Companion.Type.valueOf(it) }.getOrNull() + } ?: continue, + mcc = line[headers.indexOf(FIELD_MCC)].toIntOrNull() ?: continue, + mnc = line[headers.indexOf(FIELD_MNC)].toIntOrNull() ?: continue, + lac = line[headers.indexOf(FIELD_LAC_TAC)].toIntOrNull(), + tac = line[headers.indexOf(FIELD_LAC_TAC)].toIntOrNull(), + cid = line[headers.indexOf(FIELD_CID)].toLongOrNull() ?: continue, + pscOrPci = line[headers.indexOf(FIELD_PSC)].toIntOrNull(), + ) + if (learnCellLocation(cell, location)) counter++ + } + } + } + } + } catch (e: Exception) { + Log.w(TAG, e) + } + return counter + } + + override fun onCreate(db: SQLiteDatabase) { + migrate(db, 0, CURRENT_VERSION) + } + + fun cleanup(db: SQLiteDatabase) { + db.delete(TABLE_CELLS, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_CACHE_AGE).toString())) + db.delete(TABLE_CELLS_PRE, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_CACHE_AGE).toString())) + db.delete(TABLE_WIFIS, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_CACHE_AGE).toString())) + db.delete(TABLE_CELLS_LEARN, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_LEARN_AGE).toString())) + db.delete(TABLE_WIFI_LEARN, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_LEARN_AGE).toString())) + } + + override fun onOpen(db: SQLiteDatabase) { + super.onOpen(db) + if (!db.isReadOnly) { + cleanup(db) + } + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + migrate(db, oldVersion, newVersion) + } + + override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + migrate(db, oldVersion, newVersion) + } + + fun dump(writer: PrintWriter) { + writer.println("Database: cells(cached)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_CELLS)}, cells(learnt)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_CELLS_LEARN)}, wifis(cached)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_WIFIS)}, wifis(learnt)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_WIFI_LEARN)}") + } +} + +const val EXTRA_HIGH_LOCATION = "high" +const val EXTRA_LOW_LOCATION = "low" +const val EXTRA_RECORD_COUNT = "recs" +private const val MAX_CACHE_AGE = 1000L * 60 * 60 * 24 * 14 // 14 days +private const val MAX_LEARN_AGE = 1000L * 60 * 60 * 24 * 365 // 1 year +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 TABLE_CELLS_LEARN = "cells_learn" +private const val TABLE_WIFI_LEARN = "wifis_learn" +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_ALTITUDE = "alt" +private const val FIELD_ALTITUDE_ACCURACY = "alt_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 FIELD_LATITUDE_HIGH = "lath" +private const val FIELD_LATITUDE_LOW = "latl" +private const val FIELD_LONGITUDE_HIGH = "lonh" +private const val FIELD_LONGITUDE_LOW = "lonl" +private const val FIELD_ALTITUDE_HIGH = "alth" +private const val FIELD_ALTITUDE_LOW = "altl" +private const val FIELD_BAD_TIME = "btime" +private const val FIELD_LEARN_RECORD_COUNT = "recs" + +private const val LEARN_BASE_ACCURACY_CELL = 10_000.0 +private const val LEARN_BASE_VERTICAL_ACCURACY_CELL = 5_000.0 +private const val LEARN_BASE_PRECISION_CELL = 0.5 +private const val LEARN_ACCURACY_FACTOR_CELL = 0.002 +private const val LEARN_VERTICAL_ACCURACY_FACTOR_CELL = 0.002 +private const val LEARN_PRECISION_FACTOR_CELL = 0.01 +private const val LEARN_BAD_SIZE_CELL = 10_000 + +private const val LEARN_BASE_ACCURACY_WIFI = 200.0 +private const val LEARN_BASE_VERTICAL_ACCURACY_WIFI = 100.0 +private const val LEARN_BASE_PRECISION_WIFI = 0.2 +private const val LEARN_ACCURACY_FACTOR_WIFI = 0.02 +private const val LEARN_VERTICAL_ACCURACY_FACTOR_WIFI = 0.02 +private const val LEARN_PRECISION_FACTOR_WIFI = 0.1 +private const val LEARN_BAD_SIZE_WIFI = 200 + +private const val LEARN_BAD_CUTOFF = 1000L * 60 * 60 * 24 * 14 + +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 val FIELDS_CACHE_LOCATION = arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_ALTITUDE, FIELD_ALTITUDE_ACCURACY, FIELD_TIME, FIELD_PRECISION) +private val FIELDS_MID_LOCATION = arrayOf(FIELD_LATITUDE_HIGH, FIELD_LATITUDE_LOW, FIELD_LONGITUDE_HIGH, FIELD_LONGITUDE_LOW, FIELD_ALTITUDE_HIGH, FIELD_ALTITUDE_LOW, FIELD_LEARN_RECORD_COUNT, FIELD_TIME) +private val FIELDS_MID_LOCATION_GET_LEARN = FIELDS_MID_LOCATION + FIELD_BAD_TIME +private val FIELDS_CELL = arrayOf(FIELD_MCC, FIELD_MNC, FIELD_TYPE, FIELD_LAC_TAC, FIELD_CID, FIELD_PSC) +private val FIELDS_WIFI = arrayOf(FIELD_MAC) +private val FIELDS_EXPORT_DATA = arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ALTITUDE) +private val SUPPORTED_TYPES = listOf( + "application/vnd.microg.location.cell+csv+gzip", + "application/vnd.microg.location.cell+csv", + "application/vnd.microg.location.wifi+csv+gzip", + "application/vnd.microg.location.wifi+csv", + "application/gzip", + "application/x-gzip", + "text/csv", +) + +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.pscOrPci ?: 0).toString(), + ) +} + +private fun getWifiSelection(wifi: WifiDetails): String { + return "$FIELD_MAC = x'${wifi.macBytes.toHexString()}'" +} + +private fun getCellPreSelectionArgs(cell: CellDetails): Array { + return arrayOf( + cell.mcc.toString(), + cell.mnc.toString() + ) +} + +private fun Cursor.getSingleLocation(maxAge: Long): Location? { + return try { + if (moveToNext()) { + getLocation(maxAge) + } else { + null + } + } finally { + close() + } +} + +private fun Cursor.getLocation(maxAge: Long): Location? { + if (getLong(5) > 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() + getDoubleOrNull(3)?.let { altitude = it } + verticalAccuracy = getDoubleOrNull(4)?.toFloat() + time = getLong(5) + precision = getDouble(6) + } + } + return null +} + +private fun Cursor.getMidLocation( + maxAge: Long = Long.MAX_VALUE, + baseAccuracy: Double = 0.0, + accuracyFactor: Double = 0.0, + baseVerticalAccuracy: Double = baseAccuracy, + verticalAccuracyFactor: Double = accuracyFactor, + basePrecision: Double = 0.0, + precisionFactor: Double = 0.0 +): Location? { + if (maxAge == Long.MAX_VALUE || getLong(7) > System.currentTimeMillis() - maxAge) { + val high = Location(PROVIDER_CACHE).apply { latitude = getDouble(0); longitude = getDouble(2) } + if (!isNull(4)) high.altitude = getDouble(4) + val low = Location(PROVIDER_CACHE).apply { latitude = getDouble(1); longitude = getDouble(3) } + if (!isNull(5)) low.altitude = getDouble(5) + val count = getInt(6) + val computedAccuracy = baseAccuracy / (1 + (accuracyFactor * (count - 1).toDouble())) + val computedVerticalAccuracy = baseVerticalAccuracy / (1 + (verticalAccuracyFactor * (count - 1).toDouble())) + return Location(PROVIDER_CACHE).apply { + latitude = (high.latitude + low.latitude) / 2.0 + longitude = (high.longitude + low.longitude) / 2.0 + accuracy = max(high.distanceTo(low) / 2.0, computedAccuracy).toFloat() + if (high.hasAltitude() && low.hasAltitude()) { + altitude = (high.altitude + low.altitude) / 2.0 + verticalAccuracy = max((abs(high.altitude - low.altitude) / 2.0), computedVerticalAccuracy).toFloat() + } else if (high.hasAltitude()) { + altitude = high.altitude + verticalAccuracy = computedVerticalAccuracy.toFloat() + } else if (low.hasAltitude()) { + altitude = low.altitude + verticalAccuracy = computedVerticalAccuracy.toFloat() + } + precision = basePrecision * (1 + (precisionFactor * (count - 1))) + highLocation = high + lowLocation = low + extras += EXTRA_RECORD_COUNT to count + } + } + return null +} + +private fun Cursor.getWifiMidLocation() = getMidLocation( + MAX_LEARN_AGE, + baseAccuracy = LEARN_BASE_ACCURACY_WIFI, + accuracyFactor = LEARN_ACCURACY_FACTOR_WIFI, + baseVerticalAccuracy = LEARN_BASE_VERTICAL_ACCURACY_WIFI, + verticalAccuracyFactor = LEARN_VERTICAL_ACCURACY_FACTOR_WIFI, + basePrecision = LEARN_BASE_PRECISION_WIFI, + precisionFactor = LEARN_PRECISION_FACTOR_WIFI +) + +private fun Cursor.getCellMidLocation() = getMidLocation( + MAX_LEARN_AGE, + baseAccuracy = LEARN_BASE_ACCURACY_CELL, + accuracyFactor = LEARN_ACCURACY_FACTOR_CELL, + baseVerticalAccuracy = LEARN_BASE_VERTICAL_ACCURACY_CELL, + verticalAccuracyFactor = LEARN_VERTICAL_ACCURACY_FACTOR_CELL, + basePrecision = LEARN_BASE_PRECISION_CELL, + precisionFactor = LEARN_PRECISION_FACTOR_CELL +) + +private var Location.highLocation: Location? + get() = extras?.let { BundleCompat.getParcelable(it, EXTRA_HIGH_LOCATION, Location::class.java) } + set(value) { extras += EXTRA_HIGH_LOCATION to value } + +private val Location.highLatitude: Double + get() = highLocation?.latitude ?: latitude +private val Location.highLongitude: Double + get() = highLocation?.longitude ?: longitude +private val Location.highAltitude: Double? + get() = highLocation?.takeIf { it.hasAltitude() }?.altitude ?: altitude.takeIf { hasAltitude() } + +private var Location.lowLocation: Location? + get() = extras?.let { BundleCompat.getParcelable(it, EXTRA_LOW_LOCATION, Location::class.java) } + set(value) { extras += EXTRA_LOW_LOCATION to value } + +private val Location.lowLatitude: Double + get() = lowLocation?.latitude ?: latitude +private val Location.lowLongitude: Double + get() = lowLocation?.longitude ?: longitude +private val Location.lowAltitude: Double? + get() = lowLocation?.takeIf { it.hasAltitude() }?.altitude ?: altitude.takeIf { hasAltitude() } + +private var Location?.recordCount: Int + get() = this?.extras?.getInt(EXTRA_RECORD_COUNT, 0) ?: 0 + set(value) { this?.extras += EXTRA_RECORD_COUNT to value } + +private fun max(first: Double, second: Double?): Double { + if (second == null) return first + return max(first, second) +} + +private fun max(first: Long, second: Long?): Long { + if (second == null) return first + return max(first, second) +} + +private fun min(first: Double, second: Double?): Double { + if (second == null) return first + return min(first, second) +} + +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) + if (location.hasAltitude()) { + put(FIELD_ALTITUDE, location.altitude) + put(FIELD_ALTITUDE_ACCURACY, location.verticalAccuracy) + } + } 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) + } +} + +private fun ContentValues.putLearnLocation(location: Location, previous: Location? = null, badTime: Long? = null, import: Boolean = false) { + if (location != NEGATIVE_CACHE_ENTRY) { + put(FIELD_LATITUDE_HIGH, max(location.latitude, previous?.highLatitude)) + put(FIELD_LATITUDE_LOW, min(location.latitude, previous?.lowLatitude)) + put(FIELD_LONGITUDE_HIGH, max(location.longitude, previous?.highLongitude)) + put(FIELD_LONGITUDE_LOW, min(location.longitude, previous?.lowLongitude)) + put(FIELD_TIME, max(location.time, previous?.time ?: 0)) + if (location.hasAltitude()) { + put(FIELD_ALTITUDE_HIGH, max(location.altitude, previous?.highAltitude)) + put(FIELD_ALTITUDE_LOW, min(location.altitude, previous?.lowAltitude)) + } + put(FIELD_LEARN_RECORD_COUNT, previous.recordCount + 1) + if (badTime != null && !import) { + put(FIELD_BAD_TIME, max(badTime, previous?.time)) + } + } +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkDetails.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkDetails.kt new file mode 100644 index 0000000000..441266511b --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkDetails.kt @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network + +interface NetworkDetails { + val timestamp: Long? + val signalStrength: Int? +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt new file mode 100644 index 0000000000..6bc04af098 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationRequest.kt @@ -0,0 +1,34 @@ +/* + * 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 +import org.microg.gms.location.EXTRA_LOCATION +import org.microg.gms.location.EXTRA_ELAPSED_REALTIME + +class NetworkLocationRequest( + var pendingIntent: PendingIntent, + var intervalMillis: Long, + var lowPower: Boolean, + var bypass: Boolean, + var workSource: WorkSource +) { + var lastRealtime = 0L + private set + + fun send(context: Context, location: Location) { + lastRealtime = SystemClock.elapsedRealtime() + pendingIntent.send(context, 0, Intent().apply { + putExtra(EXTRA_LOCATION, location) + putExtra(EXTRA_ELAPSED_REALTIME, lastRealtime) + }) + } +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt new file mode 100644 index 0000000000..2292a84966 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt @@ -0,0 +1,621 @@ +/* + * 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.location.LocationManager +import android.net.Uri +import android.net.wifi.WifiManager +import android.os.* +import android.os.Build.VERSION.SDK_INT +import android.util.Log +import androidx.annotation.GuardedBy +import androidx.core.content.IntentCompat +import androidx.core.content.getSystemService +import androidx.core.location.LocationListenerCompat +import androidx.core.location.LocationManagerCompat +import androidx.core.location.LocationRequestCompat +import androidx.core.os.bundleOf +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import org.microg.gms.location.* +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.ichnaea.IchnaeaServiceClient +import org.microg.gms.location.network.wifi.* +import java.io.FileDescriptor +import java.io.PrintWriter +import java.util.* +import kotlin.math.* + +class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDetailsCallback { + private lateinit var handlerThread: HandlerThread + private lateinit var handler: Handler + + @GuardedBy("activeRequests") + private val activeRequests = HashSet() + private val highPowerScanRunnable = Runnable { this.scan(false) } + private val lowPowerScanRunnable = Runnable { this.scan(true) } + private var wifiDetailsSource: WifiDetailsSource? = null + private var cellDetailsSource: CellDetailsSource? = null + private val ichnaea by lazy { IchnaeaServiceClient(this) } + private val database by lazy { LocationDatabase(this) } + private val movingWifiHelper by lazy { MovingWifiHelper(this) } + private val settings by lazy { LocationSettings(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 passiveLocationListener by lazy { LocationListenerCompat { onNewPassiveLocation(it) } } + + @GuardedBy("gpsLocationBuffer") + private val gpsLocationBuffer = LinkedList() + private var passiveListenerActive = false + + private var currentLocalMovingWifi: WifiDetails? = null + private var lastLocalMovingWifiLocationCandidate: 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 = WifiDetailsSource.create(this, this).apply { enable() } + cellDetailsSource = CellDetailsSource.create(this, this).apply { enable() } + if (settings.effectiveEndpoint == null && (settings.wifiIchnaea || settings.cellIchnaea)) { + sendBroadcast(Intent(ACTION_CONFIGURATION_REQUIRED).apply { + `package` = packageName + putExtra(EXTRA_CONFIGURATION, CONFIGURATION_FIELD_ONLINE_SOURCE) + }) + } + } + + private fun updatePassiveGpsListenerRegistration() { + try { + getSystemService()?.let { locationManager -> + if ((settings.cellLearning || settings.wifiLearning) && (highPowerIntervalMillis != Long.MAX_VALUE)) { + if (!passiveListenerActive) { + LocationManagerCompat.requestLocationUpdates( + locationManager, + LocationManager.PASSIVE_PROVIDER, + LocationRequestCompat.Builder(LocationRequestCompat.PASSIVE_INTERVAL) + .setQuality(LocationRequestCompat.QUALITY_LOW_POWER) + .setMinUpdateIntervalMillis(GPS_PASSIVE_INTERVAL) + .build(), + passiveLocationListener, + handlerThread.looper + ) + passiveListenerActive = true + } + } else { + if (passiveListenerActive) { + LocationManagerCompat.removeUpdates(locationManager, passiveLocationListener) + passiveListenerActive = false + } + } + } + } catch (e: SecurityException) { + Log.d(TAG, "GPS location retriever not initialized due to lack of permission") + } catch (e: Exception) { + Log.d(TAG, "GPS location retriever not initialized", e) + } + } + + @Suppress("DEPRECATION") + @SuppressLint("WrongConstant") + private fun scan(lowPower: Boolean) { + if (!lowPower) lastHighPowerScanRealtime = SystemClock.elapsedRealtime() + lastLowPowerScanRealtime = SystemClock.elapsedRealtime() + val currentLocalMovingWifi = currentLocalMovingWifi + val lastWifiScanIsSufficientlyNewForMoving = lastWifiDetailsRealtimeMillis > SystemClock.elapsedRealtime() - MAX_LOCAL_WIFI_SCAN_AGE_MS + val movingWifiWasProducingRecentResults = (lastLocalMovingWifiLocationCandidate?.elapsedMillis ?: 0L) > SystemClock.elapsedRealtime() - max(MAX_LOCAL_WIFI_AGE_MS, interval * 2) + val movingWifiLocationWasAccurate = (lastLocalMovingWifiLocationCandidate?.accuracy ?: Float.MAX_VALUE) <= MOVING_WIFI_HIGH_POWER_ACCURACY + if (currentLocalMovingWifi != null && + movingWifiWasProducingRecentResults && + lastWifiScanIsSufficientlyNewForMoving && + (movingWifiLocationWasAccurate || lowPower) && + getSystemService()?.connectionInfo?.bssid == currentLocalMovingWifi.macAddress + ) { + Log.d(TAG, "Skip network scan and use current local wifi instead. low=$lowPower accurate=$movingWifiLocationWasAccurate") + onWifiDetailsAvailable(listOf(currentLocalMovingWifi.copy(timestamp = System.currentTimeMillis()))) + } else { + val workSource = synchronized(activeRequests) { activeRequests.minByOrNull { it.intervalMillis }?.workSource } + Log.d(TAG, "Start network scan for $workSource") + if (settings.wifiLearning || settings.wifiCaching || settings.wifiIchnaea) { + wifiDetailsSource?.startScan(workSource) + } else if (settings.wifiMoving) { + // No need to scan if only moving wifi enabled, instead simulate scan based on current connection info + val connectionInfo = getSystemService()?.connectionInfo + if (SDK_INT >= 31 && connectionInfo != null && connectionInfo.toWifiDetails() != null) { + onWifiDetailsAvailable(listOfNotNull(connectionInfo.toWifiDetails())) + } else if (currentLocalMovingWifi != null && connectionInfo?.bssid == currentLocalMovingWifi.macAddress) { + onWifiDetailsAvailable(listOf(currentLocalMovingWifi.copy(timestamp = System.currentTimeMillis()))) + } else { + // Can't simulate scan, so just scan + wifiDetailsSource?.startScan(workSource) + } + } + if (settings.cellLearning || settings.cellCaching || settings.cellIchnaea) { + 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) + } + } + + updatePassiveGpsListenerRegistration() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent?.action == ACTION_NETWORK_LOCATION_SERVICE) { + handler.post { + val pendingIntent = IntentCompat.getParcelableExtra(intent, EXTRA_PENDING_INTENT, PendingIntent::class.java) ?: 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 = IntentCompat.getParcelableExtra(intent, EXTRA_WORK_SOURCE, WorkSource::class.java) ?: 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() } + } + } + } else if (intent?.action == ACTION_NETWORK_IMPORT_EXPORT) { + handler.post { + val callback = IntentCompat.getParcelableExtra(intent, EXTRA_MESSENGER, Messenger::class.java) + val replyWhat = intent.getIntExtra(EXTRA_REPLY_WHAT, 0) + when (intent.getStringExtra(EXTRA_DIRECTION)) { + DIRECTION_EXPORT -> { + val name = intent.getStringExtra(EXTRA_NAME) + val uri = name?.let { database.exportLearned(it) } + callback?.send(Message.obtain().apply { + what = replyWhat + data = bundleOf( + EXTRA_DIRECTION to DIRECTION_EXPORT, + EXTRA_NAME to name, + EXTRA_URI to uri, + ) + }) + } + DIRECTION_IMPORT -> { + val uri = IntentCompat.getParcelableExtra(intent, EXTRA_URI, Uri::class.java) + val counter = uri?.let { database.importLearned(it) } ?: 0 + callback?.send(Message.obtain().apply { + what = replyWhat + arg1 = counter + data = bundleOf( + EXTRA_DIRECTION to DIRECTION_IMPORT, + EXTRA_URI to uri, + ) + }) + } + } + } + } + super.onStartCommand(intent, flags, startId) + return START_STICKY + } + + override fun onDestroy() { + handlerThread.quitSafely() + wifiDetailsSource?.disable() + wifiDetailsSource = null + cellDetailsSource?.disable() + cellDetailsSource = null + super.onDestroy() + } + + fun Location.mayTakeAltitude(location: Location?) { + if (location != null && !hasAltitude() && location.hasAltitude()) { + altitude = location.altitude + verticalAccuracy = location.verticalAccuracy + } + } + + suspend fun queryWifiLocation(wifis: List): Location? { + var candidate: Location? = queryCurrentLocalMovingWifiLocation() + if ((candidate?.accuracy ?: Float.MAX_VALUE) <= 50f) return candidate + val databaseCandidate = queryWifiLocationFromDatabase(wifis) + if (databaseCandidate != null && (candidate == null || databaseCandidate.precision > candidate.precision)) { + databaseCandidate.mayTakeAltitude(candidate) + candidate = databaseCandidate + } + if ((candidate?.accuracy ?: Float.MAX_VALUE) <= 50f && (candidate?.precision ?: 0.0) > 1.0) return candidate + val ichnaeaCandidate = queryIchnaeaWifiLocation(wifis, minimumPrecision = (candidate?.precision ?: 0.0)) + if (ichnaeaCandidate != null && ichnaeaCandidate.accuracy >= (candidate?.accuracy ?: 0f)) { + ichnaeaCandidate.mayTakeAltitude(candidate) + candidate = ichnaeaCandidate + } + return candidate + } + + private suspend fun queryCurrentLocalMovingWifiLocation(): Location? { + var candidate: Location? = null + val currentLocalMovingWifi = currentLocalMovingWifi + if (currentLocalMovingWifi != null && settings.wifiMoving) { + try { + withTimeout(5000L) { + lastLocalMovingWifiLocationCandidate = movingWifiHelper.retrieveMovingLocation(currentLocalMovingWifi) + } + candidate = lastLocalMovingWifiLocationCandidate + } catch (e: Exception) { + lastLocalMovingWifiLocationCandidate = null + Log.w(TAG, "Failed retrieving location for current moving wifi ${currentLocalMovingWifi.ssid}", e) + } + } + return candidate + } + + private suspend fun queryWifiLocationFromDatabase(wifis: List): Location? = + queryLocationFromRetriever(wifis, 1000.0) { database.getWifiLocation(it, settings.wifiLearning) } + + private suspend fun queryCellLocationFromDatabase(cells: List): Location? = + queryLocationFromRetriever(cells, 50000.0) { it.location ?: database.getCellLocation(it, settings.cellLearning) } + + private val NetworkDetails.signalStrengthBounded: Int + get() = (signalStrength ?: -100).coerceIn(-100, -10) + + private val NetworkDetails.ageBounded: Long + get() = (System.currentTimeMillis() - (timestamp ?: 0)).coerceIn(0, 60000) + + private val NetworkDetails.weight: Double + get() = min(1.0, sqrt(2000.0 / ageBounded)) / signalStrengthBounded.toDouble().pow(2) + + private fun queryLocationFromRetriever(data: List, maxClusterDistance: Double = 0.0, retriever: (T) -> Location?): Location? { + val locations = data.mapNotNull { detail -> retriever(detail)?.takeIf { it != NEGATIVE_CACHE_ENTRY }?.let { detail to it } } + if (locations.isNotEmpty()) { + val clusters = locations.map { mutableListOf(it) } + for (cellLocation in locations) { + for (cluster in clusters) { + if (cluster.first() == cellLocation) continue; + if (cluster.first().second.distanceTo(cellLocation.second) < max(cluster.first().second.accuracy * 2.0, maxClusterDistance)) { + cluster.add(cellLocation) + } + } + } + val cluster = clusters.maxBy { it.sumOf { it.second.precision } } + + return Location(PROVIDER_CACHE).apply { + latitude = cluster.weightedAverage { it.second.latitude to it.first.weight } + longitude = cluster.weightedAverage { it.second.longitude to it.first.weight } + accuracy = min( + cluster.map { it.second.distanceTo(this) + it.second.accuracy }.average().toFloat() / cluster.size, + cluster.minOf { it.second.accuracy } + ) + val altitudeCluster = cluster.filter { it.second.hasAltitude() }.takeIf { it.isNotEmpty() } + if (altitudeCluster != null) { + altitude = altitudeCluster.weightedAverage { it.second.altitude to it.first.weight } + verticalAccuracy = min( + altitudeCluster.map { abs(altitude - it.second.altitude) + (it.second.verticalAccuracy ?: it.second.accuracy) }.average().toFloat() / cluster.size, + altitudeCluster.minOf { it.second.verticalAccuracy ?: it.second.accuracy } + ) + } + precision = cluster.sumOf { it.second.precision } + time = System.currentTimeMillis() + } + } + return null + } + + private suspend fun queryIchnaeaWifiLocation(wifis: List, minimumPrecision: Double = 0.0): Location? { + if (settings.wifiIchnaea && wifis.size >= 3 && wifis.size / IchnaeaServiceClient.WIFI_BASE_PRECISION_COUNT >= minimumPrecision) { + try { + val ichnaeaCandidate = ichnaea.retrieveMultiWifiLocation(wifis) { wifi, location -> + if (settings.wifiCaching) database.putWifiLocation(wifi, location) + }!! + ichnaeaCandidate.time = System.currentTimeMillis() + return ichnaeaCandidate + } catch (e: Exception) { + Log.w(TAG, "Failed retrieving location for ${wifis.size} wifi networks", e) + } + } + return null + } + + private fun List.weightedAverage(f: (T) -> Pair): Double { + val valuesAndWeights = map { f(it) } + return valuesAndWeights.sumOf { it.first * it.second } / valuesAndWeights.sumOf { it.second } + } + + override fun onWifiDetailsAvailable(wifis: List) { + if (wifis.isEmpty()) return + val scanResultTimestamp = min(wifis.maxOf { it.timestamp ?: Long.MAX_VALUE }, System.currentTimeMillis()) + val scanResultRealtimeMillis = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - scanResultTimestamp) + @Suppress("DEPRECATION") + currentLocalMovingWifi = getSystemService()?.connectionInfo + ?.let { wifiInfo -> wifis.filter { it.macAddress == wifiInfo.bssid && it.isMoving } } + ?.filter { movingWifiHelper.isLocallyRetrievable(it) } + ?.singleOrNull() + val requestableWifis = wifis.filter(WifiDetails::isRequestable) + if (requestableWifis.isEmpty() && currentLocalMovingWifi == null) return + updateWifiLocation(requestableWifis, scanResultRealtimeMillis, scanResultTimestamp) + } + + private fun updateWifiLocation(requestableWifis: List, scanResultRealtimeMillis: Long = 0, scanResultTimestamp: Long = 0) { + if (settings.wifiLearning) { + for (wifi in requestableWifis.filter { it.timestamp != null }) { + val wifiElapsedMillis = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - wifi.timestamp!!) + getGpsLocation(wifiElapsedMillis)?.let { + database.learnWifiLocation(wifi, it) + } + } + } + if (scanResultRealtimeMillis < lastWifiDetailsRealtimeMillis + interval / 2 && lastWifiDetailsRealtimeMillis != 0L && scanResultRealtimeMillis != 0L) { + Log.d(TAG, "Ignoring wifi details, similar age as last ($scanResultRealtimeMillis < $lastWifiDetailsRealtimeMillis + $interval / 2)") + return + } + val previousLastRealtimeMillis = lastWifiDetailsRealtimeMillis + if (scanResultRealtimeMillis != 0L) lastWifiDetailsRealtimeMillis = scanResultRealtimeMillis + lifecycleScope.launch { + val location = queryWifiLocation(requestableWifis) + if (location == null) { + lastWifiDetailsRealtimeMillis = previousLastRealtimeMillis + return@launch + } + if (scanResultTimestamp != 0L) location.time = max(scanResultTimestamp, location.time) + if (scanResultRealtimeMillis != 0L) location.elapsedRealtimeNanos = max(location.elapsedRealtimeNanos, scanResultRealtimeMillis * 1_000_000L) + synchronized(locationLock) { + lastWifiLocation = location + } + sendLocationUpdate() + } + } + + override fun onWifiSourceFailed() { + // Wifi source failed, create a new one + wifiDetailsSource?.disable() + wifiDetailsSource = WifiDetailsSource.create(this, this).apply { enable() } + } + + private suspend fun queryCellLocation(cells: List): Location? { + val candidate = queryCellLocationFromDatabase(cells) + if ((candidate?.precision ?: 0.0) > 1.0) return candidate + val cellsToUpdate = cells.filter { it.location == null && database.getCellLocation(it, settings.cellLearning) == null } + for (cell in cellsToUpdate) { + queryIchnaeaCellLocation(cell) + } + // Try again after fetching records from internet + return queryCellLocationFromDatabase(cells) + } + + private suspend fun queryIchnaeaCellLocation(cell: CellDetails): Location? { + if (settings.cellIchnaea) { + try { + val ichnaeaCandidate = ichnaea.retrieveSingleCellLocation(cell) { cell, location -> + if (settings.cellCaching) database.putCellLocation(cell, location) + } ?: NEGATIVE_CACHE_ENTRY + if (settings.cellCaching) { + if (ichnaeaCandidate == NEGATIVE_CACHE_ENTRY) { + database.putCellLocation(cell, NEGATIVE_CACHE_ENTRY) + return null + } else { + ichnaeaCandidate.time = System.currentTimeMillis() + database.putCellLocation(cell, ichnaeaCandidate) + return ichnaeaCandidate + } + } + } catch (e: Exception) { + Log.w(TAG, "Failed retrieving location for cell network", e) + } + } + return null + } + + override fun onCellDetailsAvailable(cells: List) { + if (settings.cellLearning) { + for (cell in cells.filter { it.timestamp != null && it.location == null }) { + val cellElapsedMillis = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - cell.timestamp!!) + getGpsLocation(cellElapsedMillis)?.let { + database.learnCellLocation(cell, it) + } + } + } + val scanResultTimestamp = min(cells.maxOf { it.timestamp ?: Long.MAX_VALUE }, System.currentTimeMillis()) + val scanResultRealtimeMillis = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - scanResultTimestamp) + if (scanResultRealtimeMillis < lastCellDetailsRealtimeMillis + interval / 2 && lastCellDetailsRealtimeMillis != 0L) { + Log.d(TAG, "Ignoring cell details, similar age as last ($scanResultRealtimeMillis < $lastCellDetailsRealtimeMillis + $interval / 2)") + return + } + val previousLastRealtimeMillis = lastWifiDetailsRealtimeMillis + lastCellDetailsRealtimeMillis = scanResultRealtimeMillis + lifecycleScope.launch { + val location = queryCellLocation(cells) + if (location == null) { + lastCellDetailsRealtimeMillis = previousLastRealtimeMillis + return@launch + } + if (scanResultTimestamp != 0L) location.time = max(scanResultTimestamp, location.time) + if (scanResultRealtimeMillis != 0L) location.elapsedRealtimeNanos = max(location.elapsedRealtimeNanos, scanResultRealtimeMillis * 1_000_000L) + synchronized(locationLock) { + lastCellLocation = location + } + sendLocationUpdate() + } + } + + private fun sendLocationUpdate(now: Boolean = false) { + fun cliffLocations(old: Location?, new: Location?): Location? { + // We move from wifi towards cell with accuracy + if (old == null) return new + if (new == null) return old + val diff = new.elapsedMillis - old.elapsedMillis + if (diff < LOCATION_TIME_CLIFF_START_MS) return old + if (diff > LOCATION_TIME_CLIFF_END_MS) return new + val pct = (diff - LOCATION_TIME_CLIFF_START_MS).toDouble() / (LOCATION_TIME_CLIFF_END_MS - LOCATION_TIME_CLIFF_START_MS).toDouble() + return Location(old).apply { + provider = "cliff" + latitude = old.latitude * (1.0-pct) + new.latitude * pct + longitude = old.longitude * (1.0-pct) + new.longitude * pct + accuracy = (old.accuracy * (1.0-pct) + new.accuracy * pct).toFloat() + altitude = old.altitude * (1.0-pct) + new.altitude * pct + time = (old.time.toDouble() * (1.0-pct) + new.time.toDouble() * pct).toLong() + elapsedRealtimeNanos = (old.elapsedRealtimeNanos.toDouble() * (1.0-pct) + new.elapsedRealtimeNanos.toDouble() * pct).toLong() + } + } + val location = synchronized(locationLock) { + if (lastCellLocation == null && lastWifiLocation == null) return + when { + // Only non-null + lastCellLocation == null -> lastWifiLocation + lastWifiLocation == null -> lastCellLocation + // Consider cliff end + lastCellLocation!!.elapsedMillis > lastWifiLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_END_MS -> lastCellLocation + lastWifiLocation!!.elapsedMillis > lastCellLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_START_MS -> lastWifiLocation + // Wifi out of cell range with higher precision + lastCellLocation!!.precision > lastWifiLocation!!.precision && lastWifiLocation!!.distanceTo(lastCellLocation!!) > 2 * lastCellLocation!!.accuracy -> lastCellLocation + // Consider cliff start + lastCellLocation!!.elapsedMillis > lastWifiLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_START_MS -> cliffLocations(lastWifiLocation, lastCellLocation) + else -> lastWifiLocation + } + } ?: return + if (location == lastLocation) return + if (lastLocation == lastWifiLocation && lastLocation.let { it != null && location.accuracy > it.accuracy } && !now) { + Log.d(TAG, "Debounce inaccurate location update") + 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) + } + } + } + } + + private fun onNewPassiveLocation(location: Location) { + if (location.provider != LocationManager.GPS_PROVIDER || location.accuracy > GPS_PASSIVE_MIN_ACCURACY) return + synchronized(gpsLocationBuffer) { + if (gpsLocationBuffer.isNotEmpty() && gpsLocationBuffer.last.elapsedMillis < SystemClock.elapsedRealtime() - GPS_BUFFER_SIZE * GPS_PASSIVE_INTERVAL) { + gpsLocationBuffer.clear() + } else if (gpsLocationBuffer.size >= GPS_BUFFER_SIZE) { + gpsLocationBuffer.remove() + } + gpsLocationBuffer.offer(location) + } + } + + private fun getGpsLocation(elapsedMillis: Long): Location? { + if (elapsedMillis + GPS_BUFFER_SIZE * GPS_PASSIVE_INTERVAL < SystemClock.elapsedRealtime()) return null + synchronized(gpsLocationBuffer) { + if (gpsLocationBuffer.isEmpty()) return null + for (location in gpsLocationBuffer.descendingIterator()) { + if (location.elapsedMillis in (elapsedMillis - GPS_PASSIVE_INTERVAL)..(elapsedMillis + GPS_PASSIVE_INTERVAL)) return location + if (location.elapsedMillis < elapsedMillis) return null + } + } + return null + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter, args: Array?) { + writer.println("Last scan elapsed realtime: high-power: ${lastHighPowerScanRealtime.formatRealtime()}, low-power: ${lastLowPowerScanRealtime.formatRealtime()}") + writer.println("Last scan result time: wifi: ${lastWifiDetailsRealtimeMillis.formatRealtime()}, cells: ${lastCellDetailsRealtimeMillis.formatRealtime()}") + writer.println("Interval: high-power: ${highPowerIntervalMillis.formatDuration()}, low-power: ${lowPowerIntervalMillis.formatDuration()}") + writer.println("Last wifi location: $lastWifiLocation${if (lastWifiLocation == lastLocation) " (active)" else ""}") + writer.println("Last cell location: $lastCellLocation${if (lastCellLocation == lastLocation) " (active)" else ""}") + writer.println("Wifi settings: ichnaea=${settings.wifiIchnaea} moving=${settings.wifiMoving} learn=${settings.wifiLearning}") + writer.println("Cell settings: ichnaea=${settings.cellIchnaea} learn=${settings.cellLearning}") + writer.println("Ichnaea settings: source=${settings.onlineSource?.id} endpoint=${settings.effectiveEndpoint} contribute=${settings.ichnaeaContribute}") + ichnaea.dump(writer) + writer.println("GPS location buffer size=${gpsLocationBuffer.size} first=${gpsLocationBuffer.firstOrNull()?.elapsedMillis?.formatRealtime()} last=${gpsLocationBuffer.lastOrNull()?.elapsedMillis?.formatRealtime()}") + database.dump(writer) + synchronized(activeRequests) { + if (activeRequests.isNotEmpty()) { + writer.println("Active requests:") + for (request in activeRequests) { + writer.println("- ${request.workSource} ${request.intervalMillis.formatDuration()} (low power: ${request.lowPower}, bypass: ${request.bypass}) reported ${request.lastRealtime.formatRealtime()}") + } + } + } + } + + companion object { + const val GPS_BUFFER_SIZE = 60 + const val GPS_PASSIVE_INTERVAL = 1000L + const val GPS_PASSIVE_MIN_ACCURACY = 25f + const val LOCATION_TIME_CLIFF_START_MS = 30000L + const val LOCATION_TIME_CLIFF_END_MS = 60000L + const val DEBOUNCE_DELAY_MS = 5000L + const val MAX_WIFI_SCAN_CACHE_AGE = 1000L * 60 * 60 * 24 // 1 day + const val MAX_LOCAL_WIFI_AGE_MS = 60_000_000L // 1 minute + const val MAX_LOCAL_WIFI_SCAN_AGE_MS = 600_000_000L // 10 minutes + const val MOVING_WIFI_HIGH_POWER_ACCURACY = 100f + } +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt new file mode 100644 index 0000000000..b123a4d362 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetails.kt @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.cell + +import android.location.Location +import org.microg.gms.location.network.NetworkDetails + +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 pscOrPci: Int? = null, + override val timestamp: Long? = null, + override val signalStrength: Int? = null, + val location: Location? = null +) : NetworkDetails { + companion object { + enum class Type { + CDMA, GSM, WCDMA, LTE, TDSCDMA, NR + } + } +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsCallback.kt b/play-services-location/core/provider/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/provider/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/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt new file mode 100644 index 0000000000..8e809f95a6 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/CellDetailsSource.kt @@ -0,0 +1,74 @@ +/* + * 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 android.util.Log +import androidx.core.content.getSystemService + +private const val TAG = "CellDetailsSource" + +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) { + try { + 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) + } + }) + } catch (e: SecurityException) { + // It may trigger a SecurityException if the ACCESS_FINE_LOCATION permission isn't granted + Log.w(TAG, "requestCellInfoUpdate in startScan failed", e) + } + + return + } else if (SDK_INT >= 17) { + val allCellInfo: List? = try { + telephonyManager.allCellInfo + } catch (e: SecurityException) { + // It may trigger a SecurityException if the ACCESS_FINE_LOCATION permission isn't granted + Log.w(TAG, "allCellInfo in startScan failed", e) + null + } + if (allCellInfo != null) { + val details = allCellInfo.map(CellInfo::toCellDetails).map { it.repair(context) }.filter(CellDetails::isValid) + if (details.isNotEmpty()) { + callback.onCellDetailsAvailable(details) + return + } + } + } + val networkOperator = telephonyManager.networkOperator + if (networkOperator != null && networkOperator.length > 4) { + val mcc = networkOperator.substring(0, 3).toIntOrNull() + val mnc = networkOperator.substring(3).toIntOrNull() + val detail: CellDetails? = try { + telephonyManager.cellLocation?.toCellDetails(mcc, mnc) + } catch (e: SecurityException) { + // It may trigger a SecurityException if the ACCESS_FINE_LOCATION permission isn't granted + Log.w(TAG, "cellLocation in startScan failed", e) + null + } + if (detail?.isValid == true) callback.onCellDetailsAvailable(listOf(detail)) + } + } + + companion object { + fun create(context: Context, callback: CellDetailsCallback): CellDetailsSource = CellDetailsSource(context, callback) + } +} diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt new file mode 100644 index 0000000000..19580b73df --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/cell/extensions.kt @@ -0,0 +1,159 @@ +/* + * 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 androidx.annotation.RequiresApi +import androidx.core.content.getSystemService + +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(), + pscOrPci = 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") +} + +private fun CellIdentityCdma.toCellDetails() = CellDetails( + type = CellDetails.Companion.Type.CDMA, + sid = systemId, + nid = networkId, + bsid = basestationId, + location = locationFromCdma(latitude, longitude) +) + +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() +) + +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(), + pscOrPci = psc.takeIf { it != Int.MAX_VALUE && it != -1 } +) + +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(), + pscOrPci = pci.takeIf { it != Int.MAX_VALUE && it != -1 } +) + +@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/provider/src/main/kotlin/org/microg/gms/location/network/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/extensions.kt new file mode 100644 index 0000000000..3435ba668a --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/extensions.kt @@ -0,0 +1,39 @@ +/* + * 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 androidx.core.os.bundleOf +import java.security.MessageDigest + +const val TAG = "NetworkLocation" +const val LOCATION_EXTRA_PRECISION = "precision" + +const val PROVIDER_CACHE = "cache" +const val PROVIDER_CACHE_NEGATIVE = "cache-" +val NEGATIVE_CACHE_ENTRY = Location(PROVIDER_CACHE_NEGATIVE) + +internal operator fun Bundle?.plus(pair: Pair): Bundle = this + bundleOf(pair) + +internal operator fun Bundle?.plus(other: Bundle): Bundle = when { + this == null -> other + else -> Bundle(this).apply { putAll(other) } +} + +internal var Location.precision: Double + get() = extras?.getDouble(LOCATION_EXTRA_PRECISION, 1.0) ?: 1.0 + set(value) { + extras += LOCATION_EXTRA_PRECISION to value + } + +internal var Location.verticalAccuracy: Float? + get() = if (LocationCompat.hasVerticalAccuracy(this)) LocationCompat.getVerticalAccuracyMeters(this) else null + set(value) = if (value == null) LocationCompat.removeVerticalAccuracy(this) else LocationCompat.setVerticalAccuracyMeters(this, 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/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/BluetoothBeacon.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/BluetoothBeacon.kt new file mode 100644 index 0000000000..b3d896aaee --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/BluetoothBeacon.kt @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +data class BluetoothBeacon( + /** + * The address of the Bluetooth Low Energy (BLE) beacon. + */ + val macAddress: String? = null, + /** + * The name of the BLE beacon. + */ + val name: String? = null, + /** + * The number of milliseconds since this BLE beacon was last seen. + */ + val age: Long? = null, + /** + * The measured signal strength of the BLE beacon in dBm. + */ + val signalStrength: Int? = null, +) \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/CellTower.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/CellTower.kt new file mode 100644 index 0000000000..ce5696eabb --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/CellTower.kt @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +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/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/Fallback.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/Fallback.kt new file mode 100644 index 0000000000..fb4a158ab5 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/Fallback.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +/** + * 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/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeolocateRequest.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeolocateRequest.kt new file mode 100644 index 0000000000..5120006fea --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeolocateRequest.kt @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +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 bluetoothBeacons: List? = null, + val cellTowers: List? = null, + val wifiAccessPoints: List? = null, + /** + * 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. + */ + val fallbacks: Fallback? = null, +) \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeolocateResponse.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeolocateResponse.kt new file mode 100644 index 0000000000..9fdf468e18 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeolocateResponse.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +data class GeolocateResponse( + val location: ResponseLocation? = null, + val horizontalAccuracy: Double? = null, + val fallback: String? = null, + val error: ResponseError? = null, + + // Custom + val verticalAccuracy: Double? = null, + val raw: List = emptyList(), +) \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitItem.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitItem.kt new file mode 100644 index 0000000000..6e9e5833b1 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitItem.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +data class GeosubmitItem( + /** + * The time of observation of the data, measured in milliseconds since the UNIX epoch. Can be omitted if the observation time is very recent. The age values in each section are relative to this timestamp. + */ + val timestamp: Long? = null, + /** + * The position block contains information about where and when the data was observed. + */ + val position: GeosubmitPosition? = null, + val bluetoothBeacons: List? = null, + val cellTowers: List? = null, + val wifiAccessPoints: List? = null, +) \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitPosition.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitPosition.kt new file mode 100644 index 0000000000..51150699b3 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitPosition.kt @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +data class GeosubmitPosition( + /** + * The latitude of the observation (WSG 84). + */ + val latitude: Double? = null, + /** + * The longitude of the observation (WSG 84). + */ + val longitude: Double? = null, + /** + * The accuracy of the observed position in meters. + */ + val accuracy: Double? = null, + /** + * The altitude at which the data was observed in meters above sea-level. + */ + val altitude: Double? = null, + /** + * The accuracy of the altitude estimate in meters. + */ + val altitudeAccuracy: Double? = null, + /** + * The age of the position data (in milliseconds). + */ + val age: Long? = null, + /** + * The heading field denotes the direction of travel of the device and is specified in degrees, where 0° ≤ heading < 360°, counting clockwise relative to the true north. + */ + val heading: Double? = null, + /** + * The air pressure in hPa (millibar). + */ + val pressure: Double? = null, + /** + * The speed field denotes the magnitude of the horizontal component of the device’s current velocity and is specified in meters per second. + */ + val speed: Double? = null, + /** + * The source of the position information. If the field is omitted, “gps†is assumed. The term gps is used to cover all types of satellite based positioning systems including Galileo and Glonass. Other possible values are manual for a position entered manually into the system and fused for a position obtained from a combination of other sensors or outside service queries. + */ + val source: GeosubmitSource? = null, +) \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitRequest.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitRequest.kt new file mode 100644 index 0000000000..0add7bd3d5 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitRequest.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +data class GeosubmitRequest( + val items: List? = null +) \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitSource.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitSource.kt new file mode 100644 index 0000000000..5b9d46576f --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/GeosubmitSource.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +enum class GeosubmitSource { + GPS, MANUAL, FUSED; + + override fun toString(): String { + return super.toString().lowercase() + } +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt new file mode 100644 index 0000000000..2ecc435803 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt @@ -0,0 +1,246 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.net.Uri +import android.os.Bundle +import android.os.SystemClock +import android.util.Log +import androidx.collection.LruCache +import com.android.volley.VolleyError +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import org.json.JSONObject +import org.microg.gms.location.LocationSettings +import org.microg.gms.location.formatRealtime +import org.microg.gms.location.network.* +import org.microg.gms.location.network.cell.CellDetails +import org.microg.gms.location.network.precision +import org.microg.gms.location.network.verticalAccuracy +import org.microg.gms.location.network.wifi.* +import org.microg.gms.location.provider.BuildConfig +import org.microg.gms.utils.singleInstanceOf +import java.io.PrintWriter +import java.nio.ByteBuffer +import java.util.LinkedList +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlin.math.min + +class IchnaeaServiceClient(private val context: Context) { + private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + private val settings = LocationSettings(context) + private val omitables = LinkedList() + private val cache = LruCache(REQUEST_CACHE_SIZE) + private val start = SystemClock.elapsedRealtime() + + private val hasTelephony by lazy { + context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) + } + + private fun GeolocateRequest.hash(): ByteArray? { + if (cellTowers.isNullOrEmpty() && (wifiAccessPoints?.size ?: 0) < 3 || bluetoothBeacons?.isNotEmpty() == true) return null + val minAge = min( + cellTowers?.takeIf { it.isNotEmpty() }?.minOf { it.age?.takeIf { it > 0L } ?: 0L } ?: Long.MAX_VALUE, + wifiAccessPoints?.takeIf { it.isNotEmpty() }?.minOf { it.age?.takeIf { it > 0L } ?: 0L } ?: Long.MAX_VALUE + ) + val buffer = ByteBuffer.allocate(8 + (cellTowers?.size ?: 0) * 23 + (wifiAccessPoints?.size ?: 0) * 8) + buffer.putInt(cellTowers?.size?: 0) + buffer.putInt(wifiAccessPoints?.size?: 0) + for (cell in cellTowers.orEmpty()) { + buffer.put(cell.radioType?.ordinal?.toByte() ?: -1) + buffer.putInt(cell.mobileCountryCode ?: -1) + buffer.putInt(cell.mobileNetworkCode ?: -1) + buffer.putInt(cell.locationAreaCode ?: -1) + buffer.putInt(cell.cellId ?: -1) + buffer.putInt(cell.psc ?: -1) + buffer.put(((cell.age?.let { it - minAge }?: 0L) / (60 * 1000)).toByte()) + buffer.put(((cell.signalStrength ?: 0) / 20).toByte()) + } + for (wifi in wifiAccessPoints.orEmpty()) { + buffer.put(wifi.macBytes) + buffer.put(((wifi.age?.let { it - minAge }?: 0L) / (60 * 1000)).toByte()) + buffer.put(((wifi.signalStrength ?: 0) / 20).toByte()) + } + return buffer.array().digest("SHA-256") + } + + fun isRequestable(wifi: WifiDetails): Boolean { + return wifi.isRequestable && !omitables.contains(wifi.macClean) + } + + suspend fun retrieveMultiWifiLocation(wifis: List, rawHandler: ((WifiDetails, Location) -> Unit)? = null): Location? = geoLocate( + GeolocateRequest( + considerIp = !hasTelephony, + wifiAccessPoints = wifis.filter { isRequestable(it) }.map(WifiDetails::toWifiAccessPoint), + fallbacks = Fallback(lacf = false, ipf = false) + ), + rawWifiHandler = rawHandler + )?.apply { + precision = wifis.size.toDouble() / WIFI_BASE_PRECISION_COUNT + } + + suspend fun retrieveSingleCellLocation(cell: CellDetails, rawHandler: ((CellDetails, Location) -> Unit)? = null): Location? = geoLocate( + GeolocateRequest( + considerIp = false, + radioType = cell.toCellTower().radioType, + homeMobileCountryCode = cell.toCellTower().mobileCountryCode, + homeMobileNetworkCode = cell.toCellTower().mobileNetworkCode, + cellTowers = listOf(cell.toCellTower()), + fallbacks = Fallback( + lacf = true, + ipf = false + ) + ), + rawCellHandler = rawHandler + )?.apply { + precision = if (extras?.getString(LOCATION_EXTRA_FALLBACK) != null) CELL_FALLBACK_PRECISION else CELL_DEFAULT_PRECISION + } + + private suspend fun geoLocate( + request: GeolocateRequest, + rawWifiHandler: ((WifiDetails, Location) -> Unit)? = null, + rawCellHandler: ((CellDetails, Location) -> Unit)? = null + ): Location? { + val requestHash = request.hash() + if (requestHash != null) { + val locationFromCache = cache[requestHash.toHexString()] + if (locationFromCache == NEGATIVE_CACHE_ENTRY) return null + if (locationFromCache != null) return Location(locationFromCache) + } + val response = rawGeoLocate(request) + Log.d(TAG, "$request -> $response") + for (entry in response.raw) { + if (entry.omit && entry.wifiAccessPoint?.macAddress != null) { + omitables.offer(entry.wifiAccessPoint.macAddress.lowercase().replace(":", "")) + if (omitables.size > OMITABLES_LIMIT) omitables.remove() + runCatching { rawWifiHandler?.invoke(entry.wifiAccessPoint.toWifiDetails(), NEGATIVE_CACHE_ENTRY) } + } + if (entry.omit && entry.cellTower?.radioType != null) { + runCatching { rawCellHandler?.invoke(entry.cellTower.toCellDetails(), NEGATIVE_CACHE_ENTRY) } + } + if (!entry.omit && entry.wifiAccessPoint?.macAddress != null && entry.location != null) { + val location = buildLocation(entry.location, entry.horizontalAccuracy, entry.verticalAccuracy).apply { + precision = 1.0 + } + runCatching { rawWifiHandler?.invoke(entry.wifiAccessPoint.toWifiDetails(), location) } + } + if (!entry.omit && entry.cellTower?.radioType != null && entry.location != null) { + val location = buildLocation(entry.location, entry.horizontalAccuracy, entry.verticalAccuracy).apply { + precision = 1.0 + } + runCatching { rawCellHandler?.invoke(entry.cellTower.toCellDetails(), location) } + } + } + val location = if (response.location != null) { + buildLocation(response.location, response.horizontalAccuracy, response.verticalAccuracy).apply { + if (response.fallback != null) extras = (extras ?: Bundle()).apply { putString(LOCATION_EXTRA_FALLBACK, response.fallback) } + } + } else if (response.error != null && response.error.code == 404) { + NEGATIVE_CACHE_ENTRY + } else if (response.error != null) { + throw ServiceException(response.error) + } else { + throw RuntimeException("Invalid response JSON") + } + if (requestHash != null) { + cache[requestHash.toHexString()] = if (location == NEGATIVE_CACHE_ENTRY) NEGATIVE_CACHE_ENTRY else Location(location) + } + if (location == NEGATIVE_CACHE_ENTRY) return null + return location + } + + private fun buildLocation(location: ResponseLocation, defaultHorizontalAccuracy: Double? = null, defaultVerticalAccuracy: Double? = null): Location { + return Location(PROVIDER).apply { + latitude = location.latitude + longitude = location.longitude + if (location.altitude != null) altitude = location.altitude + if (defaultHorizontalAccuracy != null && defaultHorizontalAccuracy > 0.0) accuracy = defaultHorizontalAccuracy.toFloat() + if (hasAltitude() && defaultVerticalAccuracy != null && defaultVerticalAccuracy > 0.0) verticalAccuracy = defaultVerticalAccuracy.toFloat() + if (location.horizontalAccuracy != null && location.horizontalAccuracy > 0.0) accuracy = location.horizontalAccuracy.toFloat() + if (hasAltitude() && location.verticalAccuracy != null && location.verticalAccuracy > 0.0) verticalAccuracy = location.verticalAccuracy.toFloat() + time = System.currentTimeMillis() + } + } + + private fun getRequestHeaders(): Map = buildMap { + set("User-Agent", "${BuildConfig.ICHNAEA_USER_AGENT} (Linux; Android ${android.os.Build.VERSION.RELEASE}; ${context.packageName})") + if (settings.ichnaeaContribute) { + set("X-Ichnaea-Contribute-Opt-In", "1") + } + } + + private fun continueError(continuation: Continuation, error: VolleyError) { + try { + val response = JSONObject(error.networkResponse.data.decodeToString()).toGeolocateResponse() + if (response.error != null) { + continuation.resume(response) + return + } else if (response.location?.latitude != null){ + Log.w(TAG, "Received location in response with error code") + } else { + Log.w(TAG, "Received valid json without error in response with error code") + } + } catch (_: Exception) { + } + if (error.networkResponse != null) { + continuation.resume(GeolocateResponse(error = ResponseError(error.networkResponse.statusCode, error.message))) + return + } + continuation.resumeWithException(error) + } + + private suspend fun rawGeoLocate(request: GeolocateRequest): GeolocateResponse = suspendCoroutine { continuation -> + val url = Uri.parse(settings.effectiveEndpoint).buildUpon().appendPath("v1").appendPath("geolocate").build().toString() + queue.add(object : JsonObjectRequest(Method.POST, url, request.toJson(), { + try { + continuation.resume(it.toGeolocateResponse()) + } catch (e: Exception) { + continuation.resumeWithException(e) + } + }, { + continueError(continuation, it) + }) { + override fun getHeaders(): Map = getRequestHeaders() + }) + } + + private suspend fun rawGeoSubmit(request: GeosubmitRequest): Unit = suspendCoroutine { continuation -> + val url = Uri.parse(settings.effectiveEndpoint).buildUpon().appendPath("v2").appendPath("geosubmit").build().toString() + queue.add(object : JsonObjectRequest(Method.POST, url, request.toJson(), { + continuation.resume(Unit) + }, { + continuation.resumeWithException(it) + }) { + override fun getHeaders(): Map = getRequestHeaders() + }) + } + + fun dump(writer: PrintWriter) { + writer.println("Ichnaea start=${start.formatRealtime()} omitables=${omitables.size}") + writer.println("Ichnaea request cache size=${cache.size()} hits=${cache.hitCount()} miss=${cache.missCount()} puts=${cache.putCount()} evicts=${cache.evictionCount()}") + } + + private operator fun LruCache.set(key: K, value: V) { + put(key, value) + } + + companion object { + private const val TAG = "IchnaeaLocation" + private const val PROVIDER = "ichnaea" + const val WIFI_BASE_PRECISION_COUNT = 4.0 + const val CELL_DEFAULT_PRECISION = 1.0 + const val CELL_FALLBACK_PRECISION = 0.5 + private const val OMITABLES_LIMIT = 100 + private const val REQUEST_CACHE_SIZE = 200 + const val LOCATION_EXTRA_FALLBACK = "fallback" + } +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/RadioType.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/RadioType.kt new file mode 100644 index 0000000000..ce12cd262e --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/RadioType.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +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/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/RawGeolocateEntry.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/RawGeolocateEntry.kt new file mode 100644 index 0000000000..81a00b940c --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/RawGeolocateEntry.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +data class RawGeolocateEntry( + val timestamp: Long? = null, + + val bluetoothBeacon: BluetoothBeacon? = null, + val cellTower: CellTower? = null, + val wifiAccessPoint: WifiAccessPoint? = null, + + val location: ResponseLocation? = null, + val horizontalAccuracy: Double? = null, + val verticalAccuracy: Double? = null, + + val omit: Boolean = false, +) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ResponseError.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ResponseError.kt new file mode 100644 index 0000000000..2c3b21d8b2 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ResponseError.kt @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +data class ResponseError( + val code: Int? = null, + val message: String? = null +) \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ResponseLocation.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ResponseLocation.kt new file mode 100644 index 0000000000..f12898d6fc --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ResponseLocation.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +data class ResponseLocation( + val latitude: Double, + val longitude: Double, + + // Custom + val horizontalAccuracy: Double? = null, + val altitude: Double? = null, + val verticalAccuracy: Double? = null +) \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ServiceException.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ServiceException.kt new file mode 100644 index 0000000000..db6fdcac8c --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/ServiceException.kt @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +class ServiceException(val error: ResponseError) : Exception(error.message) \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/WifiAccessPoint.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/WifiAccessPoint.kt new file mode 100644 index 0000000000..aef5140dfe --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/WifiAccessPoint.kt @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +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/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/extensions.kt new file mode 100644 index 0000000000..77e3fe4106 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/extensions.kt @@ -0,0 +1,260 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.ichnaea + +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.microg.gms.location.network.cell.CellDetails +import org.microg.gms.location.network.wifi.WifiDetails + +private fun JSONObject.getDouble(vararg names: String): Double { + for (name in names) { + if (has(name)) return getDouble(name) + } + throw JSONException("Values are all null: ${names.joinToString(", ")}") +} + +private fun JSONObject.getInt(vararg names: String): Int { + for (name in names) { + if (has(name)) return getInt(name) + } + throw JSONException("Values are all null: ${names.joinToString(", ")}") +} + +private fun JSONObject.getString(vararg names: String): String { + for (name in names) { + if (has(name)) return getString(name) + } + throw JSONException("Values are all null: ${names.joinToString(", ")}") +} + +private fun JSONObject.optDouble(vararg names: String): Double? { + for (name in names) { + if (has(name)) optDouble(name).takeIf { it.isFinite() }?.let { return it } + } + return null +} + +private fun JSONObject.optInt(vararg names: String): Int? { + for (name in names) { + runCatching { if (has(name)) return getInt(name) } + } + return null +} + +private fun JSONObject.optLong(vararg names: String): Long? { + for (name in names) { + runCatching { if (has(name)) return getLong(name) } + } + return null +} + +private fun JSONObject.optString(vararg names: String): String? { + for (name in names) { + if (has(name)) return optString(name) + } + return null +} + +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 = pscOrPci, + signalStrength = signalStrength +) + +internal fun CellTower.toCellDetails() = CellDetails( + type = when(radioType) { + RadioType.GSM -> CellDetails.Companion.Type.GSM + RadioType.WCDMA -> CellDetails.Companion.Type.WCDMA + RadioType.LTE -> CellDetails.Companion.Type.LTE + else -> throw IllegalArgumentException("Unsupported radio type") + }, + mcc = mobileNetworkCode, + mnc = mobileNetworkCode, + lac = locationAreaCode, + tac = locationAreaCode, + cid = cellId?.toLong(), + pscOrPci = 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 WifiAccessPoint.toWifiDetails() = WifiDetails( + macAddress = macAddress, + channel = channel, + frequency = frequency, + signalStrength = signalStrength, + ssid = ssid, +) + +internal fun JSONObject.toGeolocateResponse() = GeolocateResponse( + location = optJSONObject("location")?.toResponseLocation(), + horizontalAccuracy = optDouble("accuracy", "acc", "horizontalAccuracy"), + verticalAccuracy = optDouble("altAccuracy", "altitudeAccuracy", "verticalAccuracy"), + fallback = optString("fallback").takeIf { it.isNotEmpty() }, + raw = optJSONArray("raw")?.toRawGeolocateEntries().orEmpty(), + 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 (!bluetoothBeacons.isNullOrEmpty()) put("bluetoothBeacons", JSONArray(bluetoothBeacons.map(BluetoothBeacon::toJson))) + 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()) +} + +internal fun GeosubmitRequest.toJson() = JSONObject().apply { + if (items != null) put("items", JSONArray(items.map(GeosubmitItem::toJson))) +} + +private fun GeosubmitItem.toJson() = JSONObject().apply { + if (timestamp != null) put("timestamp", timestamp) + if (position != null) put("position", position.toJson()) + if (!bluetoothBeacons.isNullOrEmpty()) put("bluetoothBeacons", JSONArray(bluetoothBeacons.map(BluetoothBeacon::toJson))) + if (!cellTowers.isNullOrEmpty()) put("cellTowers", JSONArray(cellTowers.map(CellTower::toJson))) + if (!wifiAccessPoints.isNullOrEmpty()) put("wifiAccessPoints", JSONArray(wifiAccessPoints.map(WifiAccessPoint::toJson))) +} + +private fun GeosubmitPosition.toJson() = JSONObject().apply { + if (latitude != null) put("latitude", latitude) + if (longitude != null) put("longitude", longitude) + if (accuracy != null) put("accuracy", accuracy) + if (altitude != null) put("altitude", altitude) + if (altitudeAccuracy != null) put("altitudeAccuracy", altitudeAccuracy) + if (age != null) put("age", age) + if (heading != null) put("heading", heading) + if (pressure != null) put("pressure", pressure) + if (speed != null) put("speed", speed) + if (source != null) put("source", source.toString()) +} + +private fun JSONObject.toResponseLocation() = ResponseLocation( + latitude = getDouble("lat", "latitude"), + longitude = getDouble("lng", "longitude"), + altitude = optDouble("alt", "altitude"), + horizontalAccuracy = optDouble("acc", "accuracy", "horizontalAccuracy"), + verticalAccuracy = optDouble("altAccuracy", "altitudeAccuracy", "verticalAccuracy"), +) + +private fun JSONObject.toResponseError() = ResponseError( + code = optInt("code", "errorCode", "statusCode"), + message = optString("message", "msg", "statusText") +) + +private fun JSONArray.toRawGeolocateEntries(): List = + (0 until length()).mapNotNull { optJSONObject(it)?.toRawGeolocateEntry() } + +private fun JSONObject.toRawGeolocateEntry() = RawGeolocateEntry( + timestamp = optLong("time", "timestamp"), + bluetoothBeacon = optJSONObject("bluetoothBeacon")?.toBluetoothBeacon(), + cellTower = optJSONObject("cellTower")?.toCellTower(), + wifiAccessPoint = optJSONObject("wifiAccessPoint")?.toWifiAccessPoint(), + location = optJSONObject("location")?.toResponseLocation(), + horizontalAccuracy = optDouble("acc", "accuracy", "horizontalAccuracy"), + verticalAccuracy = optDouble("altAccuracy", "altitudeAccuracy", "verticalAccuracy"), + omit = optBoolean("omit") +) + +private fun JSONObject.toBluetoothBeacon() = BluetoothBeacon( + macAddress = getString("macAddress", "mac", "address"), + name = optString("name"), +) + +private fun JSONObject.toCellTower() = CellTower( + radioType = optString("radioType", "radio", "type")?.let { runCatching { RadioType.valueOf(it.uppercase()) }.getOrNull() }, + mobileCountryCode = optInt("mobileCountryCode", "mcc"), + mobileNetworkCode = optInt("mobileNetworkCode", "mnc"), + locationAreaCode = optInt("locationAreaCode", "lac", "trackingAreaCode", "tac"), + cellId = optInt("cellId", "cellIdentity", "cid"), + psc = optInt("psc", "primaryScramblingCode", "physicalCellId", "pci"), +) + +private fun JSONObject.toWifiAccessPoint() = WifiAccessPoint( + macAddress = getString("macAddress", "mac", "bssid", "address"), + channel = optInt("channel", "chan"), + frequency = optInt("frequency", "freq"), + ssid = optString("ssid"), +) + +private fun BluetoothBeacon.toJson() = JSONObject().apply { + if (macAddress != null) put("macAddress", macAddress) + if (name != null) put("name", name) + if (age != null) put("age", age) + if (signalStrength != null) put("signalStrength", signalStrength) +} + +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) +} + +val WifiAccessPoint.macClean: String + get() = macAddress.lowercase().replace(":", "") + +val WifiAccessPoint.macBytes: ByteArray + get() { + 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() + ) + } + +val GeolocateRequest.isWifiOnly: Boolean + get() = bluetoothBeacons.isNullOrEmpty() && cellTowers.isNullOrEmpty() && wifiAccessPoints?.isNotEmpty() == true + +val GeolocateRequest.isCellOnly: Boolean + get() = bluetoothBeacons.isNullOrEmpty() && wifiAccessPoints.isNullOrEmpty() && cellTowers?.isNotEmpty() == true + +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/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt new file mode 100644 index 0000000000..ac5dba5566 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt @@ -0,0 +1,587 @@ +/* + * 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.Context +import android.location.Location +import android.net.ConnectivityManager +import android.net.ConnectivityManager.TYPE_WIFI +import android.net.Network +import android.os.Build.VERSION.SDK_INT +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService +import androidx.core.location.LocationCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONArray +import org.json.JSONObject +import org.microg.gms.location.network.TAG +import java.net.HttpURLConnection +import java.net.Proxy +import java.net.URL +import java.net.URLConnection +import java.security.KeyStore +import java.security.cert.* +import java.text.SimpleDateFormat +import java.util.* +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +private val MOVING_WIFI_HOTSPOTS = setOf( + // Austria + "OEBB", + "Austrian FlyNet", + "svciob", // OEBB Service WIFI + // Belgium + "THALYSNET", + // Canada + "Air Canada", + "ACWiFi", + "ACWiFi.com", + // China + "Shenzhen Airlines", + // Colombia + "avianca", + "avaincaonair.com", + // Czech Republic + "CDWiFi", + // France + "_SNCF_WIFI_INOUI", + "_SNCF_WIFI_INTERCITES", + "_WIFI_LYRIA", + "OUIFI", + "NormandieTrainConnecte", + // Germany + "WIFIonICE", + "WIFI@DB", + "WiFi@DB", + "RRX Hotspot", + "FlixBux", + "FlixBus Wi-Fi", + "FlixTrain Wi-Fi", + "FlyNet", + "Telekom_FlyNet", + "Vestische WLAN", + "agilis-Wifi", + "freeWIFIahead!", + "metronom free WLAN", + // Greece + "AegeanWiFi", + // Hong Kong + "Cathay Pacific", + // Hungary + "MAVSTART-WIFI", + // Netherlands + "KEOLIS Nederland", + // New Zealand + "AirNZ_InflightWiFi", + "Bluebridge WiFi", + // Portugal + "TAP Air Portugal", + // Singapore + "KrisWorld", + // Sweden + "SJ", + "saswifi", + // Switzerland + "SBB-Free", + "SBB-FREE", + "SWISS Connect", + "Edelweiss Entertainment", + "LXCREW", + // Thailand + "THAI Wireless IFE", + // United Kingdom + "Avanti_Free_WiFi", + "CrossCountryWiFi", + "GWR WiFi", + "LNR On Board Wi-Fi", + "LOOP on train WiFi", + "WMR On Board Wi-Fi", + "EurostarTrainsWiFi", + // United States + "Amtrak_WiFi", +) + +private val PHONE_HOTSPOT_KEYWORDS = setOf( + "iPhone", + "Galaxy", + "AndroidAP" +) + +/** + * A Wi-Fi hotspot that changes its location dynamically and thus is unsuitable for use with location services that assume stable locations. + * + * Some moving Wi-Fi hotspots allow to determine their location when connected or through a public network API. + */ +val WifiDetails.isMoving: Boolean + get() { + if (MOVING_WIFI_HOTSPOTS.contains(ssid)) { + return true + } + if (PHONE_HOTSPOT_KEYWORDS.any { ssid?.contains(it) == true }) { + return true + } + return false + } + +const val FEET_TO_METERS = 0.3048 +const val KNOTS_TO_METERS_PER_SECOND = 0.5144 +const val MILES_PER_HOUR_TO_METERS_PER_SECOND = 0.447 + +class MovingWifiHelper(private val context: Context) { + @RequiresApi(23) + @Suppress("DEPRECATION") + private fun ConnectivityManager.getCurrentWifiNetwork(): Network? = (allNetworks.singleOrNull { + val networkInfo = getNetworkInfo(it) + networkInfo?.type == TYPE_WIFI && networkInfo.isConnected + }) + + private fun openConnection(network: Network?, url: URL, proxy: Proxy = Proxy.NO_PROXY): URLConnection = + (if (SDK_INT >= 23) network?.openConnection(url, proxy) else null) ?: url.openConnection() + + @SuppressLint("CustomX509TrustManager") + private fun disableCertificateRevocationCheck(originalTrustManager: TrustManager): TrustManager { + if (originalTrustManager is X509TrustManager) { + return object : X509TrustManager { + override fun checkClientTrusted(chain: Array?, authType: String?) { + Log.d(TAG, "checkClientTrusted: $chain, $authType") + originalTrustManager.checkClientTrusted(chain, authType) + } + + override fun checkServerTrusted(chain: Array?, authType: String?) { + Log.d(TAG, "checkServerTrusted: $chain, $authType") + originalTrustManager.checkServerTrusted(chain, authType) + } + + override fun getAcceptedIssuers(): Array { + return originalTrustManager.acceptedIssuers + } + } + } else { + return originalTrustManager + } + } + + private fun disableRevocationChecks(connection: HttpsURLConnection) { + try { + val ctx = SSLContext.getInstance("TLS") + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + val ks = KeyStore.getInstance("AndroidCAStore") + ks.load(null, null) + tmf.init(ks) + ctx.init(null, tmf.trustManagers.map(::disableCertificateRevocationCheck).toTypedArray(), null) + connection.sslSocketFactory = ctx.socketFactory + } catch (e: Exception) { + Log.w(TAG, "Failed to disable revocation", e) + } + } + + private fun tryConnections( + networks: List, + url: URL, + proxy: Proxy = Proxy.NO_PROXY, + use: (connection: HttpURLConnection) -> T + ): Pair> { + val exceptions = mutableListOf() + for (network in networks) { + val connection = openConnection(network, url, proxy) as HttpURLConnection + try { + try { + connection.doInput = true + if (connection is HttpsURLConnection && SDK_INT >= 24) disableRevocationChecks(connection) + if (connection.responseCode != 200) throw RuntimeException("Got error") + } catch (e: Exception) { + exceptions.add(e) + continue + } + try { + return use(connection) to emptyList() + } catch (e: Exception) { + exceptions.add(e) + break + } + } finally { + try { + connection.inputStream.close() + connection.disconnect() + } catch (ignored: Exception) { + } + } + } + return null to exceptions + } + + suspend fun retrieveMovingLocation(current: WifiDetails): Location { + if (!isLocallyRetrievable(current)) throw IllegalArgumentException() + val connectivityManager = context.getSystemService() ?: throw IllegalStateException() + val network = if (SDK_INT >= 23) connectivityManager.getCurrentWifiNetwork() else null + val sources = MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE[current.ssid]!! + val allExceptions = mutableListOf() + for (source in sources) { + val url = URL(source.url) + val (location, exceptions) = withContext(Dispatchers.IO) { + tryConnections(listOfNotNull(network) + null, url) { + val location = Location(current.ssid ?: "wifi") + source.parse(location, it.inputStream.readBytes()) + } + } + if (location != null) return location + allExceptions.addAll(exceptions) + } + if (allExceptions.size == 1) throw allExceptions.single() + val e = RuntimeException("${allExceptions.size} attempts failed") + allExceptions.forEach { e.addSuppressed(it) } + throw e + } + + fun isLocallyRetrievable(wifi: WifiDetails): Boolean = + MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE.containsKey(wifi.ssid) + + companion object { + abstract class MovingWifiLocationSource(val url: String) { + abstract fun parse(location: Location, data: ByteArray): Location + } + + private val SOURCE_WIFI_ON_ICE = object : MovingWifiLocationSource("https://iceportal.de/api1/rs/status") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + if (json.getString("gpsStatus") != "VALID") throw RuntimeException("GPS not valid") + location.accuracy = 100f + location.time = json.getLong("serverTime") - 15000L + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + } + + private val SOURCE_OEBB_1 = object : MovingWifiLocationSource("https://railnet.oebb.at/assets/modules/fis/combined.json") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()).getJSONObject("latestStatus") + location.accuracy = 100f + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("dateTime"))?.time }.getOrNull()?.let { location.time = it } + location.latitude = json.getJSONObject("gpsPosition").getDouble("latitude") + location.longitude = json.getJSONObject("gpsPosition").getDouble("longitude") + json.getJSONObject("gpsPosition").optDouble("orientation").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + } + + private val SOURCE_OEBB_2 = object : MovingWifiLocationSource("https://railnet.oebb.at/api/gps") { + override fun parse(location: Location, data: ByteArray): Location { + val root = JSONObject(data.decodeToString()) + if (root.has("JSON")) { + val json = root.getJSONObject("JSON") + if (!json.isNull("error")) throw RuntimeException("Error: ${json.get("error")}"); + location.accuracy = 100f + location.latitude = json.getDouble("lat") + location.longitude = json.getDouble("lon") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + } else if (root.optDouble("Latitude").let { !it.isNaN() && it.isFinite() && it > 0.1 }) { + location.accuracy = 100f + location.latitude = root.getDouble("Latitude") + location.longitude = root.getDouble("Longitude") + } else { + throw RuntimeException("Unsupported: $root") + } + return location + } + } + + private val SOURCE_FLIXBUS = object : MovingWifiLocationSource("https://media.flixbus.com/services/pis/v1/position") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + } + + class PassengeraLocationSource(base: String) : MovingWifiLocationSource("$base/portal/api/vehicle/realtime") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("gpsLat") + location.longitude = json.getDouble("gpsLng") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it } + return location + } + } + + private val SOURCE_PASSENGERA_MAV = PassengeraLocationSource("http://portal.mav.hu") + private val SOURCE_PASSENGERA_CD = PassengeraLocationSource("http://cdwifi.cz") + + private val SOURCE_DISPLAY_UGO = object : MovingWifiLocationSource("https://api.ife.ugo.aero/navigation/positions") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONArray(data.decodeToString()).getJSONObject(0) + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("created_at"))?.time }.getOrNull()?.let { location.time = it } + json.optDouble("speed_kilometers_per_hour").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude_meters").takeIf { !it.isNaN() }?.let { location.altitude = it } + json.optDouble("bearing_in_degree").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + } + + private val SOURCE_INFLIGHT_PANASONIC = object : MovingWifiLocationSource("https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getJSONObject("current_coordinates").getDouble("latitude") + location.longitude = json.getJSONObject("current_coordinates").getDouble("longitude") + json.optDouble("ground_speed_knots").takeIf { !it.isNaN() }?.let { + location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude_feet").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } + json.optDouble("true_heading_degree").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + } + + class BoardConnectLocationSource(base: String) : MovingWifiLocationSource("$base/map/api/flightData") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("lat") + location.longitude = json.getDouble("lon") + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("utc"))?.time }.getOrNull()?.let { location.time = it } + json.optDouble("groundSpeed").takeIf { !it.isNaN() }?.let { + location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } + json.optDouble("heading").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + } + + private val SOURCE_LUFTHANSA_FLYNET_EUROPE = BoardConnectLocationSource("https://www.lufthansa-flynet.com") + private val SOURCE_LUFTHANSA_FLYNET_EUROPE_2 = BoardConnectLocationSource("https://ww2.lufthansa-flynet.com") + private val SOURCE_AUSTRIAN_FLYNET_EUROPE = BoardConnectLocationSource("https://www.austrian-flynet.com") + private val SOURCE_SWISS_CONNECT_EUROPE = BoardConnectLocationSource("https://connect.swiss.com") + + class SncfLocationSource(base: String) : MovingWifiLocationSource("$base/router/api/train/gps") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + if (json.has("fix") && json.getInt("fix") == -1) throw RuntimeException("GPS not valid") + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + location.time = json.getLong("timestamp") + json.optDouble("heading").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + } + + private val SOURCE_SNCF = SncfLocationSource("https://wifi.sncf") + private val SOURCE_SNCF_INTERCITES = SncfLocationSource("https://wifi.intercites.sncf") + private val SOURCE_NORMANDIE = SncfLocationSource("https://wifi.normandie.fr") + + private val SOURCE_LYRIA = object : MovingWifiLocationSource("https://wifi.tgv-lyria.com/api/train/gps/position/") { + /* If there is no location available (e.g. in a tunnel), the API + endpoint returns HTTP 500, though it may reuse a previous + location for a few seconds. The returned JSON has a + "satellites" integer field, but this always seems to be 0, even + when there is a valid location available. + */ + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + // Speed is returned in m/s. + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it } + return location + } + } + + private val SOURCE_OUIFI = object : MovingWifiLocationSource("https://ouifi.ouigo.com:8084/api/gps") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + if (json.has("fix") && json.getInt("fix") == -1) throw RuntimeException("GPS not valid") + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + location.time = json.getLong("timestamp") + json.optDouble("heading").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + } + + private val SOURCE_OMBORD = object : MovingWifiLocationSource("https://www.ombord.info/api/jsonp/position/") { + override fun parse(location: Location, data: ByteArray): Location { + // The API endpoint returns a JSONP object (even when no ?callback= is supplied), so strip the surrounding function call. + val json = JSONObject(data.decodeToString().trim().trim('(', ')', ';')) + // TODO: what happens in the Channel Tunnel? Does "satellites" go to zero? Does "mode" change? + if (json.has("satellites") && json.getInt("satellites") < 1) throw RuntimeException("Ombord has no GPS fix") + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + location.time = json.getLong("time") + // "cmg" means "course made good", i.e. the compass heading of the track over the ground. + // Sometimes gets stuck for a few minutes, so use a generous accuracy value. + json.optDouble("cmg").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { + location.altitude = it + } + return location + } + } + + private val SOURCE_AIR_CANADA = object : MovingWifiLocationSource("https://airbornemedia.inflightinternet.com/asp/api/flight/info") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()).getJSONObject("gpsData") + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optLong("utcTime").takeIf { it != 0L }?.let { location.time = it } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } + json.optDouble("horizontalVelocity").takeIf { !it.isNaN() }?.let { + location.speed = (it * MILES_PER_HOUR_TO_METERS_PER_SECOND).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + } + + private val SOURCE_HOTSPLOTS = object : MovingWifiLocationSource("http://hsp.hotsplots.net/status.json") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("lat") + location.longitude = json.getDouble("lng") + json.optLong("ts").takeIf { it != 0L }?.let { location.time = it * 1000 } + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + } + + class ZiiLocationSource(prefix: String) : MovingWifiLocationSource("https://$prefix.mediasuite.zii.aero:8483/fp3d_logs/last") { + override fun parse(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + if (!json.optBoolean("positionValid")) throw RuntimeException("GPS not valid") + location.accuracy = 100f + location.latitude = json.getDouble("presentLat") + location.longitude = json.getDouble("presentLon") + json.optLong("time").takeIf { it != 0L }?.let { location.time = it * 1000 } + json.optDouble("groundSpeedKnots").takeIf { !it.isNaN() }?.let { + location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("trueHeading").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + } + + private val SOURCE_ZII_THAI = ZiiLocationSource("tha") + private val SOURCE_ZII_LUFTHANSA = ZiiLocationSource("dlh") + + private val MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE: Map> = mapOf( + "WIFIonICE" to listOf(SOURCE_WIFI_ON_ICE), + "OEBB" to listOf(SOURCE_OEBB_2, SOURCE_OEBB_1), + "FlixBus" to listOf(SOURCE_FLIXBUS), + "FlixBus Wi-Fi" to listOf(SOURCE_FLIXBUS), + "FlixTrain Wi-Fi" to listOf(SOURCE_FLIXBUS), + "MAVSTART-WIFI" to listOf(SOURCE_PASSENGERA_MAV), + "AegeanWiFi" to listOf(SOURCE_DISPLAY_UGO), + "Telekom_FlyNet" to listOf(SOURCE_INFLIGHT_PANASONIC, SOURCE_ZII_LUFTHANSA), + "Cathay Pacific" to listOf(SOURCE_INFLIGHT_PANASONIC), + "KrisWorld" to listOf(SOURCE_INFLIGHT_PANASONIC), + "SWISS Connect" to listOf(SOURCE_INFLIGHT_PANASONIC, SOURCE_SWISS_CONNECT_EUROPE), + "Edelweiss Entertainment" to listOf(SOURCE_INFLIGHT_PANASONIC), + "TAP Air Portugal" to listOf(SOURCE_INFLIGHT_PANASONIC), + "FlyNet" to listOf(SOURCE_LUFTHANSA_FLYNET_EUROPE, SOURCE_LUFTHANSA_FLYNET_EUROPE_2), + "CDWiFi" to listOf(SOURCE_PASSENGERA_CD), + "Air Canada" to listOf(SOURCE_AIR_CANADA), + "ACWiFi" to listOf(SOURCE_AIR_CANADA), + "ACWiFi.com" to listOf(SOURCE_AIR_CANADA), + "OUIFI" to listOf(SOURCE_OUIFI), + "_SNCF_WIFI_INOUI" to listOf(SOURCE_SNCF), + "_SNCF_WIFI_INTERCITES" to listOf(SOURCE_SNCF_INTERCITES), + "_WIFI_LYRIA" to listOf(SOURCE_LYRIA), + "NormandieTrainConnecte" to listOf(SOURCE_NORMANDIE), + "agilis-Wifi" to listOf(SOURCE_HOTSPLOTS), + "Austrian FlyNet" to listOf(SOURCE_AUSTRIAN_FLYNET_EUROPE), + "EurostarTrainsWiFi" to listOf(SOURCE_OMBORD), + "THAI Wireless IFE" to listOf(SOURCE_ZII_THAI), + "Shenzhen Airlines" to listOf(SOURCE_INFLIGHT_PANASONIC), + ) + } +} diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt new file mode 100644 index 0000000000..c2c3e88a8e --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetails.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.wifi + +import org.microg.gms.location.network.NetworkDetails + +data class WifiDetails( + val macAddress: String, + val ssid: String? = null, + val frequency: Int? = null, + val channel: Int? = null, + override val timestamp: Long? = null, + override val signalStrength: Int? = null, + val open: Boolean = false +): NetworkDetails diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt new file mode 100644 index 0000000000..82cb06d298 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsCallback.kt @@ -0,0 +1,11 @@ +/* + * 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) + fun onWifiSourceFailed() +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiDetailsSource.kt b/play-services-location/core/provider/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/provider/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/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt new file mode 100644 index 0000000000..71cbb4180a --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiManagerSource.kt @@ -0,0 +1,41 @@ +/* + * 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 + +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(org.microg.gms.location.network.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/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt new file mode 100644 index 0000000000..b86eb0f8a6 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/WifiScannerSource.kt @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.wifi + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.net.wifi.WifiScanner +import android.os.Build.VERSION.SDK_INT +import android.os.WorkSource +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import org.microg.gms.location.network.TAG + +@SuppressLint("WrongConstant") +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().apply { + band = WifiScanner.WIFI_BAND_BOTH + }, object : WifiScanner.ScanListener { + override fun onSuccess() { + Log.d(TAG, "Not yet implemented: onSuccess") + failed = false + } + + override fun onFailure(reason: Int, description: String?) { + Log.d(TAG, "Not yet implemented: onFailure $reason $description") + failed = true + callback.onWifiSourceFailed() + } + + @Deprecated("Not supported on all devices") + override fun onPeriodChanged(periodInMs: Int) { + Log.d(TAG, "Not yet implemented: onPeriodChanged $periodInMs") + } + + override fun onResults(results: Array) { + val wifis = results.flatMap { it.results.toList() }.map(ScanResult::toWifiDetails).toMutableList() + + // Some devices may have incomplete scan data to preserve battery. Ironically, this may even result + // in the actively connected wifi being missing from the results. Add them manually to ensure local + // moving wifi location works properly + if (SDK_INT >= 31) { + @Suppress("DEPRECATION") + val connectedWifi = context.getSystemService()?.connectionInfo?.toWifiDetails() + if (connectedWifi != null && wifis.none { it.macClean == connectedWifi.macClean }) { + wifis += connectedWifi + } + } + + callback.onWifiDetailsAvailable(wifis) + } + + override fun onFullResult(fullScanResult: ScanResult) { + Log.d(TAG, "Not yet implemented: onFullResult $fullScanResult") + } + }, workSource) + } + + companion object { + private var failed = false + fun isSupported(context: Context): Boolean { + return SDK_INT >= 26 && !failed && (context.getSystemService("wifiscanner") as? WifiScanner) != null && ContextCompat.checkSelfPermission(context, Manifest.permission.LOCATION_HARDWARE) == PERMISSION_GRANTED + } + } +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt new file mode 100644 index 0000000000..d0be1c7376 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt @@ -0,0 +1,100 @@ +/* + * 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.net.wifi.WifiInfo +import android.net.wifi.WifiManager +import android.os.Build.VERSION.SDK_INT +import android.os.SystemClock +import androidx.annotation.RequiresApi + +internal fun ScanResult.toWifiDetails(): WifiDetails = WifiDetails( + macAddress = BSSID, + ssid = SSID, + timestamp = if (SDK_INT >= 19) System.currentTimeMillis() - (SystemClock.elapsedRealtime() - (timestamp / 1000)) else null, + frequency = frequency, + channel = frequencyToChannel(frequency), + signalStrength = level, + open = setOf("WEP", "WPA", "PSK", "EAP", "IEEE8021X", "PEAP", "TLS", "TTLS").none { capabilities.contains(it) } +) + +@RequiresApi(31) +internal fun WifiInfo.toWifiDetails(): WifiDetails? { + return WifiDetails( + macAddress = bssid ?: return null, + ssid = ssid?.removeSurrounding("\"") + ?.takeIf { it != WifiManager.UNKNOWN_SSID }, + timestamp = System.currentTimeMillis(), + frequency = frequency, + signalStrength = rssi, + open = currentSecurityType == WifiInfo.SECURITY_TYPE_OPEN + ) +} + +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 + } +} + +val WifiDetails.isNomap: Boolean + get() = ssid?.endsWith("_nomap") == true + +val WifiDetails.isHidden: Boolean + get() = ssid == "" + +val WifiDetails.isRequestable: Boolean + get() = !isNomap && !isHidden && !isMoving + +val WifiDetails.macBytes: ByteArray + get() { + 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() + ) + } +val WifiDetails.macClean: String + get() = macAddress.lowercase().replace(":", "") \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt new file mode 100644 index 0000000000..c737ea25ba --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/AbstractLocationProviderPreTiramisu.kt @@ -0,0 +1,70 @@ +/* + * 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.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/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt new file mode 100644 index 0000000000..c9b4a10999 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/FusedLocationProviderService.kt @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Intent +import android.location.Criteria +import android.location.Location +import android.os.Binder +import android.os.Build.VERSION.SDK_INT +import android.util.Log +import androidx.core.location.LocationRequestCompat +import com.android.location.provider.ProviderPropertiesUnbundled +import com.android.location.provider.ProviderRequestUnbundled +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority +import kotlin.math.max + +class FusedLocationProviderService : IntentLocationProviderService() { + override fun extractLocation(intent: Intent): Location? = LocationResult.extractResult(intent)?.lastLocation + + @SuppressLint("MissingPermission") + override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) { + val intervalMillis = max(currentRequest?.interval ?: Long.MAX_VALUE, minIntervalMillis) + val request = LocationRequest.Builder(intervalMillis) + if (SDK_INT >= 31 && currentRequest != null) { + request.setPriority(when { + currentRequest.interval == LocationRequestCompat.PASSIVE_INTERVAL -> Priority.PRIORITY_PASSIVE + currentRequest.isLowPower -> Priority.PRIORITY_LOW_POWER + currentRequest.quality == LocationRequestCompat.QUALITY_LOW_POWER -> Priority.PRIORITY_LOW_POWER + currentRequest.quality == LocationRequestCompat.QUALITY_HIGH_ACCURACY -> Priority.PRIORITY_HIGH_ACCURACY + else -> Priority.PRIORITY_BALANCED_POWER_ACCURACY + }) + request.setMaxUpdateDelayMillis(currentRequest.maxUpdateDelayMillis) + request.setWorkSource(currentRequest.workSource) + } else { + request.setPriority(when { + currentRequest?.interval == LocationRequestCompat.PASSIVE_INTERVAL -> Priority.PRIORITY_PASSIVE + else -> Priority.PRIORITY_BALANCED_POWER_ACCURACY + }) + } + if (SDK_INT >= 29 && currentRequest != null) { + request.setBypass(currentRequest.isLocationSettingsIgnored) + } + val identity = Binder.clearCallingIdentity() + try { + LocationServices.getFusedLocationProviderClient(this).requestLocationUpdates(request.build(), pendingIntent) + } catch (e: SecurityException) { + Log.d(TAG, "Failed requesting location updated", e) + } + Binder.restoreCallingIdentity(identity) + } + + override fun stopIntentUpdated(pendingIntent: PendingIntent) { + LocationServices.getFusedLocationProviderClient(this).removeLocationUpdates(pendingIntent) + } + + override val minIntervalMillis: Long + get() = MIN_INTERVAL_MILLIS + override val minReportMillis: Long + get() = MIN_REPORT_MILLIS + override val properties: ProviderPropertiesUnbundled + get() = PROPERTIES + override val providerName: String + get() = "fused" + + companion object { + private const val MIN_INTERVAL_MILLIS = 1000L + private const val MIN_REPORT_MILLIS = 1000L + 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/provider/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GenericLocationProvider.kt new file mode 100644 index 0000000000..4f7e1a6edc --- /dev/null +++ b/play-services-location/core/provider/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 reportLocationToSystem(location: Location) +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt new file mode 100644 index 0000000000..3f4c2034ea --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/GeocodeProviderService.kt @@ -0,0 +1,29 @@ +/* + * 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 +import java.io.FileDescriptor +import java.io.PrintWriter + +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 + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array?) { + provider?.dump(writer) + } +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.kt new file mode 100644 index 0000000000..39c8642601 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderPreTiramisu.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_UPDATE_CURRENT +import android.content.Intent +import android.location.Location +import android.location.LocationManager +import android.os.Build.VERSION.SDK_INT +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.os.WorkSource +import androidx.annotation.RequiresApi +import androidx.core.app.PendingIntentCompat +import androidx.core.content.getSystemService +import com.android.location.provider.ProviderPropertiesUnbundled +import com.android.location.provider.ProviderRequestUnbundled +import org.microg.gms.location.elapsedMillis +import org.microg.gms.location.formatRealtime +import java.io.PrintWriter +import kotlin.math.max + +class IntentLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu { + @Deprecated("Use only with SDK < 31") + constructor(service: IntentLocationProviderService, properties: ProviderPropertiesUnbundled, legacy: Unit) : super(properties) { + this.service = service + } + + @RequiresApi(31) + constructor(service: IntentLocationProviderService, properties: ProviderPropertiesUnbundled) : super(service, properties) { + this.service = service + } + + private val service: IntentLocationProviderService + private var enabled = false + private var currentRequest: ProviderRequestUnbundled? = null + private var pendingIntent: PendingIntent? = null + private var lastReportedLocation: Location? = null + private var lastReportTime: Long = 0 + private val handler = Handler(Looper.getMainLooper()) + private val reportAgainRunnable = Runnable { reportAgain() } + + private fun updateRequest() { + if (enabled && pendingIntent != null) { + service.requestIntentUpdated(currentRequest, pendingIntent!!) + reportAgain() + } + } + + override fun dump(writer: PrintWriter) { + writer.println("Enabled: $enabled") + writer.println("Current request: $currentRequest") + if (SDK_INT >= 31) writer.println("Current work source: ${currentRequest?.workSource}") + writer.println("Last reported: $lastReportedLocation") + writer.println("Last report time: ${lastReportTime.formatRealtime()}") + } + + override fun onSetRequest(request: ProviderRequestUnbundled, source: WorkSource) { + synchronized(this) { + currentRequest = request + updateRequest() + } + } + + override fun enable() { + synchronized(this) { + if (enabled) throw IllegalStateException() + val intent = Intent(service, service.javaClass) + intent.action = ACTION_REPORT_LOCATION + pendingIntent = PendingIntentCompat.getService(service, 0, intent, FLAG_UPDATE_CURRENT, true) + currentRequest = null + enabled = true + when { + SDK_INT >= 30 -> isAllowed = true + SDK_INT >= 29 -> isEnabled = true + } + try { + if (lastReportedLocation == null) { + lastReportedLocation = service.getSystemService()?.getLastKnownLocation(service.providerName) + } + } catch (_: SecurityException) { + } catch (_: Exception) { + } + } + } + + override fun disable() { + synchronized(this) { + if (!enabled || pendingIntent == null) throw IllegalStateException() + service.stopIntentUpdated(pendingIntent!!) + pendingIntent?.cancel() + pendingIntent = null + currentRequest = null + enabled = false + handler.removeCallbacks(reportAgainRunnable) + } + } + + private fun reportAgain() { + // Report location again if it's recent enough + lastReportedLocation?.let { + if (it.elapsedMillis + max(currentRequest?.interval ?: 0, service.minIntervalMillis) > SystemClock.elapsedRealtime()) { + reportLocationToSystem(it) + } + } + } + + override fun reportLocationToSystem(location: Location) { + handler.removeCallbacks(reportAgainRunnable) + location.provider = service.providerName + lastReportedLocation = location + lastReportTime = SystemClock.elapsedRealtime() + super.reportLocation(location) + val repeatInterval = max(service.minReportMillis, currentRequest?.interval ?: Long.MAX_VALUE) + if (repeatInterval < service.minIntervalMillis) { + handler.postDelayed(reportAgainRunnable, repeatInterval) + } + } +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderService.kt new file mode 100644 index 0000000000..f0483d9dd7 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/IntentLocationProviderService.kt @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.location.Location +import android.os.Binder +import android.os.Build.VERSION.SDK_INT +import android.os.Handler +import android.os.HandlerThread +import android.os.IBinder +import android.os.Process +import android.util.Log +import com.android.location.provider.ProviderPropertiesUnbundled +import com.android.location.provider.ProviderRequestUnbundled +import java.io.FileDescriptor +import java.io.PrintWriter + +abstract class IntentLocationProviderService : 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(this.javaClass.simpleName) + handlerThread.start() + handler = Handler(handlerThread.looper) + } + + abstract fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) + + abstract fun stopIntentUpdated(pendingIntent: PendingIntent) + + abstract fun extractLocation(intent: Intent): Location? + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (Binder.getCallingUid() == Process.myUid() && intent?.action == ACTION_REPORT_LOCATION) { + handler.post { + val location = extractLocation(intent) + if (location != null) { + provider?.reportLocationToSystem(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 -> + IntentLocationProviderPreTiramisu(this, properties) + + else -> + @Suppress("DEPRECATION") + (IntentLocationProviderPreTiramisu(this, properties, 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() + } + + abstract val minIntervalMillis: Long + abstract val minReportMillis: Long + abstract val properties: ProviderPropertiesUnbundled + abstract val providerName: String +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderService.kt new file mode 100644 index 0000000000..4231a2f03d --- /dev/null +++ b/play-services-location/core/provider/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.PendingIntent +import android.content.Intent +import android.location.Criteria +import android.location.Location +import android.location.LocationManager +import android.os.Build.VERSION.SDK_INT +import com.android.location.provider.ProviderPropertiesUnbundled +import com.android.location.provider.ProviderRequestUnbundled +import org.microg.gms.location.* +import org.microg.gms.location.network.LOCATION_EXTRA_PRECISION +import kotlin.math.max + +class NetworkLocationProviderService : IntentLocationProviderService() { + override fun extractLocation(intent: Intent): Location? = intent.getParcelableExtra(EXTRA_LOCATION)?.apply { + extras?.remove(LOCATION_EXTRA_PRECISION) + } + + override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) { + val forceNow: Boolean + val intervalMillis: Long + if (currentRequest?.reportLocation == true) { + forceNow = true + intervalMillis = max(currentRequest.interval ?: Long.MAX_VALUE, minIntervalMillis) + } else { + forceNow = false + intervalMillis = Long.MAX_VALUE + } + val intent = Intent(ACTION_NETWORK_LOCATION_SERVICE) + intent.`package` = packageName + intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent) + intent.putExtra(EXTRA_ENABLE, true) + intent.putExtra(EXTRA_INTERVAL_MILLIS, intervalMillis) + intent.putExtra(EXTRA_FORCE_NOW, forceNow) + if (SDK_INT >= 31) { + intent.putExtra(EXTRA_LOW_POWER, currentRequest?.isLowPower ?: false) + intent.putExtra(EXTRA_WORK_SOURCE, currentRequest?.workSource) + } + if (SDK_INT >= 29) { + intent.putExtra(EXTRA_BYPASS, currentRequest?.isLocationSettingsIgnored ?: false) + } + startService(intent) + } + + override fun stopIntentUpdated(pendingIntent: PendingIntent) { + val intent = Intent(ACTION_NETWORK_LOCATION_SERVICE) + intent.`package` = packageName + intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent) + intent.putExtra(EXTRA_ENABLE, false) + startService(intent) + } + + override val minIntervalMillis: Long + get() = MIN_INTERVAL_MILLIS + override val minReportMillis: Long + get() = MIN_REPORT_MILLIS + override val properties: ProviderPropertiesUnbundled + get() = PROPERTIES + override val providerName: String + get() = LocationManager.NETWORK_PROVIDER + + + companion object { + private const val MIN_INTERVAL_MILLIS = 20000L + private const val MIN_REPORT_MILLIS = 1000L + 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/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt new file mode 100644 index 0000000000..3104f548ef --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt @@ -0,0 +1,188 @@ +/* + * 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 org.microg.gms.location.LocationSettings +import org.microg.gms.utils.singleInstanceOf +import java.io.PrintWriter +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 = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + private val formatter = runCatching { Formatter() }.getOrNull() + private val addressCache = LruCache(CACHE_SIZE) + private val settings by lazy { LocationSettings(context) } + + 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" + if (!settings.geocoderNominatim) return "disabled" + 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" + if (!settings.geocoderNominatim) return "disabled" + 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 + } + + fun dump(writer: PrintWriter?) { + writer?.println("Enabled: ${settings.geocoderNominatim}") + writer?.println("Address cache: size=${addressCache.size()} hits=${addressCache.hitCount()} miss=${addressCache.missCount()} puts=${addressCache.putCount()} evicts=${addressCache.evictionCount()}") + } + + 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/provider/src/main/kotlin/org/microg/gms/location/provider/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/extensions.kt new file mode 100644 index 0000000000..96eb62ec86 --- /dev/null +++ b/play-services-location/core/provider/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.location.GeocoderParams +import android.os.Build.VERSION.SDK_INT +import com.google.android.gms.location.internal.ClientIdentity + +const val TAG = "LocationProvider" + +const val ACTION_REPORT_LOCATION = "org.microg.gms.location.provider.ACTION_REPORT_LOCATION" + +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/provider/src/main/res/xml/location_exported_files.xml b/play-services-location/core/provider/src/main/res/xml/location_exported_files.xml new file mode 100644 index 0000000000..58d8187613 --- /dev/null +++ b/play-services-location/core/provider/src/main/res/xml/location_exported_files.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/play-services-location/core/src/huawei/AndroidManifest.xml b/play-services-location/core/src/huawei/AndroidManifest.xml new file mode 100644 index 0000000000..3f3bfbde3a --- /dev/null +++ b/play-services-location/core/src/huawei/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/play-services-location/core/src/huawei/kotlin/org/microg/gms/location/manager/AskPermissionNotificationActivity.kt b/play-services-location/core/src/huawei/kotlin/org/microg/gms/location/manager/AskPermissionNotificationActivity.kt new file mode 100644 index 0000000000..6c3991179e --- /dev/null +++ b/play-services-location/core/src/huawei/kotlin/org/microg/gms/location/manager/AskPermissionNotificationActivity.kt @@ -0,0 +1,270 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.Manifest.permission.* +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.graphics.Typeface +import android.net.Uri +import android.os.Build.VERSION.SDK_INT +import android.os.Bundle +import android.provider.Settings +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.util.Log +import android.view.View +import android.widget.TextView +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.PendingIntentCompat +import androidx.core.content.getSystemService +import org.microg.gms.location.core.BuildConfig +import org.microg.gms.location.core.R +import org.microg.gms.utils.getApplicationLabel + +private const val ACTION_ASK = "org.microg.gms.location.manager.ASK_PERMISSION" +private const val ACTION_CANCEL = "org.microg.gms.location.manager.ASK_PERMISSION_CANCEL" + +@RequiresApi(23) +class AskPermissionNotificationActivity : AppCompatActivity() { + + private val foregroundRequestCode = 5 + private val backgroundRequestCode = 55 + private val sharedPreferences by lazy { + getSharedPreferences(SHARED_PREFERENCE_NAME, MODE_PRIVATE) + } + private lateinit var hintView: View + + private lateinit var rationaleTextView: TextView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (intent?.action == ACTION_CANCEL) { + hideLocationPermissionNotification(this) + finish() + return + } + + setContentView(R.layout.extended_permission_request) + rationaleTextView = findViewById(R.id.rationale_textview) + + if (checkAllPermissions()) { + hideLocationPermissionNotification(this) + finish() + return + } else if (isGranted(ACCESS_COARSE_LOCATION) && isGranted(ACCESS_FINE_LOCATION) && !isGranted(ACCESS_BACKGROUND_LOCATION) && SDK_INT >= 29) { + requestBackground() + } else { + requestForeground() + } + + + findViewById(R.id.open_setting_tv).setOnClickListener { + val intent = Intent() + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + val uri = Uri.fromParts("package", packageName, null) + intent.data = uri + startActivityForResult(intent, 123) + } + + findViewById(R.id.decline_remind_tv).setOnClickListener { + val editor = sharedPreferences.edit() + editor.putBoolean(PERMISSION_REJECT_SHOW, true) + editor.apply() + finish() + } + + hintView = findViewById(R.id.hint_sl) + + val hintTitle = getString(R.string.permission_hint_title) + val builder = SpannableStringBuilder(hintTitle + getString(R.string.permission_hint)) + val span = ForegroundColorSpan(Color.BLACK) + builder.setSpan(span, 0, hintTitle.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + builder.setSpan(StyleSpan(Typeface.BOLD), 0, hintTitle.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + + val hintContentTv = findViewById(R.id.hint_content_tv) + hintContentTv.text = builder + hintView.visibility = View.VISIBLE + } + + private fun checkAllPermissions(): Boolean { + if (SDK_INT < 23) return true + return if (SDK_INT >= 29) { + isGranted(ACCESS_COARSE_LOCATION) + && isGranted(ACCESS_FINE_LOCATION) + && isGranted(ACCESS_BACKGROUND_LOCATION) + } else { + isGranted(ACCESS_COARSE_LOCATION) + && isGranted(ACCESS_FINE_LOCATION) + } + } + + private fun isGranted(permission: String): Boolean { + return checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED + } + + private fun checkAndAddPermission(list: ArrayList, permission: String) { + val result = checkSelfPermission(permission) + Log.i(TAG, "$permission: $result") + if (result != PackageManager.PERMISSION_GRANTED) { + list.add(permission) + } + } + + private fun requestForeground() { + val appName = packageManager.getApplicationLabel(packageName) + rationaleTextView.text = getString(R.string.rationale_foreground_permission, appName) + val permissions = arrayListOf() + + if (BuildConfig.FORCE_SHOW_BACKGROUND_PERMISSION.isNotEmpty()) { + permissions.add(BuildConfig.FORCE_SHOW_BACKGROUND_PERMISSION) + } + checkAndAddPermission(permissions, ACCESS_COARSE_LOCATION) + checkAndAddPermission(permissions, ACCESS_FINE_LOCATION) + if (SDK_INT == 29) { + rationaleTextView.text = getString(R.string.rationale_permission, appName) + checkAndAddPermission(permissions, ACCESS_BACKGROUND_LOCATION) + } + requestPermissions(permissions, foregroundRequestCode) + } + + private fun requestBackground() { + rationaleTextView.setText(R.string.rationale_background_permission) + val permissions = arrayListOf() + if (BuildConfig.FORCE_SHOW_BACKGROUND_PERMISSION.isNotEmpty()) { + permissions.add(BuildConfig.FORCE_SHOW_BACKGROUND_PERMISSION) + } + if (SDK_INT >= 29) { + checkAndAddPermission(permissions, ACCESS_BACKGROUND_LOCATION) + } + requestPermissions(permissions, backgroundRequestCode) + } + + private fun requestPermissions(permissions: ArrayList, requestCode: Int) { + if (permissions.isNotEmpty()) { + Log.w(TAG, "Request permissions: $permissions") + ActivityCompat.requestPermissions(this, permissions.toTypedArray(), requestCode) + } else { + Log.i(TAG, "All permission granted") + setResult(RESULT_OK) + finish() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + Log.d(TAG, "onActivityResult: ") + checkPermissions() + } + + private fun checkPermissions() { + val permissions = mutableListOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION) + if (SDK_INT >= 29) permissions.add(ACCESS_BACKGROUND_LOCATION) + + if (permissions.all { checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED }) { + Log.d(TAG, "location permission is all granted") + hideLocationPermissionNotification(this) + finish() + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + when (requestCode) { + foregroundRequestCode -> { + for (i in permissions.indices) { + val p = permissions[i] + val grant = grantResults[i] + val msg = if (grant == PackageManager.PERMISSION_GRANTED) "GRANTED" else "DENIED" + Log.w(TAG, "$p: $grant - $msg") + } + requestBackground() + } + + backgroundRequestCode -> { + if (isGranted(ACCESS_BACKGROUND_LOCATION)) { + hideLocationPermissionNotification(this) + setResult(RESULT_OK) + finish() + } else { + reject() + } + } + + else -> { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + reject() + } + } + } + + private fun reject() { + hintView.visibility = View.VISIBLE + } + + + companion object { + private const val SHARED_PREFERENCE_NAME = "location_perm_notify" + const val PERMISSION_REJECT_SHOW = "permission_reject_show" + private const val NOTIFICATION_ID = 1026359765 + private const val ASK_REQUEST_CODE = 1026359766 + private const val CANCEL_REQUEST_CODE = 1026359767 + + @JvmStatic + fun showLocationPermissionNotification(context: Context) { + val appName = context.packageManager.getApplicationLabel(context.packageName).toString() + val title = context.getString(R.string.location_permission_notification_title, appName) + val backgroundPermissionOption = + if (SDK_INT >= 30) context.packageManager.backgroundPermissionOptionLabel else context.getString(R.string.location_permission_background_option_name) + val text = context.getString(R.string.location_permission_notification_content, backgroundPermissionOption, appName) + val notification = NotificationCompat.Builder(context, createNotificationChannel(context)) + .setContentTitle(title).setContentText(text) + .setSmallIcon(R.drawable.ic_permission_notification) + .setContentIntent(PendingIntentCompat.getActivity(context, ASK_REQUEST_CODE, Intent(context, AskPermissionNotificationActivity::class.java).apply { action = ACTION_ASK }, 0, false)) + .setStyle(NotificationCompat.BigTextStyle().bigText(text)) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setOngoing(true) + .setDeleteIntent(PendingIntentCompat.getActivity(context, CANCEL_REQUEST_CODE, Intent(context, AskPermissionNotificationActivity::class.java).apply { action = ACTION_CANCEL}, 0, false)) + .build() + context.getSystemService()?.notify(NOTIFICATION_ID, notification) + } + + @JvmStatic + fun hideLocationPermissionNotification(context: Context) { + context.getSystemService()?.cancel(NOTIFICATION_ID) + } + + @TargetApi(26) + private fun createNotificationChannel(context: Context): String { + val channelId = "missing-location-permission" + if (SDK_INT >= 26) { + val channel = NotificationChannel(channelId, "Missing location permission", NotificationManager.IMPORTANCE_HIGH) + channel.setSound(null, null) + channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + channel.setShowBadge(true) + if (SDK_INT >= 29) { + channel.setAllowBubbles(false) + } + channel.vibrationPattern = longArrayOf(0) + context.getSystemService()?.createNotificationChannel(channel) + return channel.id + } + return channelId + } + + } +} \ No newline at end of file diff --git a/play-services-location/core/src/huawei/res/drawable-zh/permission_step_1.jpg b/play-services-location/core/src/huawei/res/drawable-zh/permission_step_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2d3cfa3b7fec3184b8f1a1e03c1cd4317ad3a49b GIT binary patch literal 38513 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 literal 0 HcmV?d00001 diff --git a/play-services-location/core/src/huawei/res/drawable/arrow.png b/play-services-location/core/src/huawei/res/drawable/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..e057a7ad4642ed57a75e91636ac31d05fce9e227 GIT binary patch literal 297 zcmeAS@N?(olHy`uVBq!ia0vp^?La)Akr_z7TobV$NU;U@gt-3y|DU0wO3Wit9w>6p z)5S5w^uKbx=9`0YZ+7vBm^6JnAhO~YgTXWd))@v(feV;e*Yc)I$ztaD0e0s!&$dYb?M literal 0 HcmV?d00001 diff --git a/play-services-location/core/src/huawei/res/drawable/ic_app.png b/play-services-location/core/src/huawei/res/drawable/ic_app.png new file mode 100644 index 0000000000000000000000000000000000000000..8dd61f7e846fde159fe3045a2823405b6d02c791 GIT binary patch literal 8784 zcmV-WBCp+vP)9c-@wyPY90CJm@+W~oq;U*C z$0r|N<9Wt(#%@LqxsZz&FM13Yf@|zzOe5#DF^5`)xdq5n*#e_5>KJ~8(DDFdB4Zyp zJ;o)`8NQUISfE5e3VvB0q{#QlaZ1Y|7UqDtU{07@W5b-aFsJ`nKr9k+;cbKge$SY~ zNYYFuL>N;VS&{@KAd4UgcIlcfycFh$x!y~<4*&(clC@$~E;$q0j~IuYc5%u@r%1wB zNfvj3IcS6ubJI^3bH^Im>(-@xNaX(m;-x^DP^#|#gBc4LXQdT{r_sWQ3j_KaARv=I z)L|`H(_q~?14Qyhpgg?-`S~9hKTC~6be_g&q^bhxne@xT8vj_99s+U|A?GJgpBoHh ztk)@662~9HlPX{>Mj~O&gLHa8t{2FU$`dF__dJ-KZe5DZZH&sVZqRZi-MXQJU{^W{ zh!E0w9Xw|x{+%CBNey0fR#HYqS9jRrKNS$>%Pj0TmUbs2S3jWkjHXJ5@n^Qr}x&|pvnme7s`w2&d+nPFm|;8@>maz zMC|eNQc_a-=xtGDM6#Y2(Vm}Y`ZIY-N=i`0Ab-%ngxA1HsR{VGd>w%I8prb!Xe2s! z?p)U4!-uO@I3rO;o;-Q7DL+5zkK}>z&!0aJ1=HEHXNi-0;`vqxc+Z(LXS6Xe7REGQ zr;<^G>Wt?ZyJN==?V!K3jo%81M%K}zM^UQFYOB)81LMMjz~DwAs0OaVp4Tr3B|o?h zhhy)=i4z;>ZRB@Cms{yUJn`j~O&)|Lgd7;u6A$Apq=&*BFc;(edVU?X8G{}B>d zJ}c;PK^{!pr%gh9%3!3*@HXxObHf}f!6RvV9`J|>2?@jWHmh`zZ00x{%+K>N(ThGj z0OpS#=W4)QF=ropB<+A&0ZT+gL}$J2DoyChyhl5d0e9(Q@|FPkMO7k>5tuvHU|g3~ zfNhZ6XJ%&ZVw=h1G?kYRk+ZiD#4dTX7Powq_u!L@^4XummAk+iuoho=BiIP73l3%D zVLM?%--~d6K^)WX5ahuGEzf)K3Gzib?rUI8J}UjSd?-IZpQ56o?tu-%!^3?`BD(DM zYO!0~g9-T}bfJc)ih-s24AoCrgqN_W6kQZrp z6S3KBj&D1ax4bC(;sXyWL(I9v*%VY#VN6LM>bzyw55ty^lYH zt<1b!I+m3|iMDLY;flrhdtbuJa3;4o;^X5tvTe1Sgc|LxM5Om|I2^b!lExq7WK;oe z)u=?(C~!FFNM;5tICOx%+_QroSiP89&6z}vXMU~Wx|tKG(F`Z#&zdkge?Og`+OUGY z-Wy3j9Eqmn92+@YjISEdiBV6g09)qdIxp*eknl_fMnfj$is5gB)-M(}V{dMC?+K_OJQh01!!+2d4fKDtd z@k=mvY;5f2Ypn1>();A)x3Y+iSx6ivXL*(2a0qU^Z)b86;syKXM;_8lMI4Czio(>-$ zLHiQ!(#Qk+8b63eZXChk#cW_-pw9Gh)OX1&icU{cRMEs9KDwYF8#n{R9~U~^zk%e^IA&JwVT%9DJ*C(mLfDJCML#@7%`$5 zIN(tjli@2nJ6r2Ki}0lkn52rt897f(%3n=nDGSs0{Mlq!UjXI(tH1n28M(QpHFyK) z&;l={^@P@}S@R#@K)5GiOuVTMS6}}U-bZQpQjz=Q7o^eQlow4TDGz%S2GKpsKQbBE zN?aTE6^-1yj&cjsmh%#-y%16rgvVXGc1-~X!YfK38NL|xii(ONRTD@}?o+^}de{}! zmb;i2^(eKNHQHooc!{RpO{CUyCsDh3Q?z^~Wd2mTg`eYRH_rNo>T*S8d|!FM>tA8d zlcq3i8KK$>fj~I1c;v{DDCPjZ(OR)EMq=OV;K75Pc(7Bdt1t90j7q2|vh3TLJkCCk zCwOtgh)YI>hxpUFWi)12IBic!bh-O(FUY6;ocvDsHIn}J?=>2kjASta&Sq};i4=Xc;jxVw1}V%<|u zJq34Fzp$`yi)sRS`^}1if^06?|70SCyV#$2Kc(AN6S~&Mu{{MyI43%an$DU?^~1(% zIZtkZISg72#m7CgJc!enoy>&>;DlRwkj#H3OqkG|d6OpcABgb+9syr!GcMeHIOurR zY>&N9Ov+y<)BcN#an)eKq5bs!_RZR9M&s{%0qEAKZ!e&2t|lZTWIg-rvu(i%!Hx1I z-07-n$Hc_o&s}RbDZ*GTF5nT2i>?R?9GRT^j5HC$9UMq{fD`X+rZKA^!tY zIwK>ac+Q+TcY_l%X3V&HS6U>{by8DP|11({W#oZNG%mOzu;uPC5yKrI=SjD%HH}vt zp(+m&iS$Jz(koW17y(YWl}HN$)yvDv`#|JBrN+TMg}2Sa__Wa`a$FsQC?RbOsmyu& z8LFC!AP{^9#@xJl^OxX+TLcm>s?S;dRMiQzH;2Xw9N8X)uju3_$)0!EG+uRu%EM-; z#4uh&M8pDc!Yu-Q`st_0fg11|*Qh3tHwpCV_%&qvxep~pcB5naZ>4?lgH2>{h2)G$ zR8zbU03E}77BAH8zo?Je174nNH?s zo5raLP_?BYn~5h*oH+Z*C!gGQjk^Tm$6idH3n~fZ)gG9mpuz+yUeMk=P7Q%-0;Q#; z6@K>FX9K~FatS0_)u{LTGI@UU8v@yqspPxsOe84}#S25t6gjE2l36z?e zdf~$lKST(t|H><`l)a%U2^1C<)}049t+ER9brM~yCDA#iacTlo9dAV+NK|2G-h1!8 z{@{jN1R67DOecPGZdE2w$?Rry`ou2NI5h#P2vmxHSdx>I^Iv-DCH(cn@(CnQBEI)qS92$BtzU9XhlNxFNPPT#F9Js>3uC6!7InEyYkxFJZFp=bHIKS;3s%ghLjJo#%+PxFEZQF+99L)qb z$|n#fNe%h&hV1O@X(|Z>cyplals%P-e!Rm(kgK71!7X(9_zu%})fK7^l;Hz^QGB&$ z!-fsG?cEK68)YBoU802ZbA+(6&R& zV^%+?KGKT6>a%p|(yzdYiV&ztL`1|e5U5m9BN6a6kv@~W)kF@rFrzM=jhFnW!F2+@Z{2#Yd?)V?+WOn78f>fD6%Mw01Iy$3w z@80MEHM;G#+t5TTmq3u(EnBv1#E&f<4##FeAZ3ZPS9@YWzKs*(b|!MT3v{&4Mt`R^ zCn|7I{*tYf-Te*9?({0Z$AavH9`i z$G=fsi6KUN(+>i)o{)!LNCW3Yh3g-uQ^`?GB=h)ggWp@QFOKZ5Po?Y*FH?5Amz)S0 zNx7pJkt5T0dvPjnt{^KgeCL1V%9V4#1;L4OOL!@@9)0vtu&vqFty`a0ok#=T-rnVu zEs=AfA!cG!07YLlrrgJ0queikAV+*EowQfrHZ1&FNBjxO9lMNdga3!J+Y938(S^tU z=faAO>jf;hz01Xnj0~DIY0}%^g5ZQ(1PXfa!3U8@U;pmA@AfM$F3wlECkA-Cv8pL7 zZmqW8Wh92XIQ6&wmyB)e`6k)l4x_wjKanFQkxu5i2tT=bR1g(QdEc%k`y10JyW8t# z(iqY470Ug5iBCwRa03<4_P~xFJ$iEZ@ZooX3!;g5)%*t%gk-5%vt~{Bc^h_P+f^sh z-fnAXaOBhZ?eB3os(2{-?O!d)-glVU;2e5vecmEl|50Qc^xtH=`vbDw^AXvGyzl&3 zuQw?>c%=D#1X@b%!Gi}k=EqjYjvX7T znm~YxTl}7~B~a1Qz9xc{k05e9^N8!PV->o-PPyZ_I?Gp>xa***c?ie`VqRVzEnd8M zCUc z$(6$C)ViR^g_P%r|MKu=@5fR6>?*tAA zF1Q`OAb4?|Awz~V<;ONhjvSe21%cGu?$?Pku`8KGtxTlRmY zq~`njs&gNG_*%Gd;cVD`;J|^nTYdT8i7!KB9(2<;-+c2RPgLo*G$g#+V7(Hj)1o+> zbukeKDEhiF+57y(WKdticGvq_C1;$sr%?UJR%G~!kB=|n3jAr<-jn=C3SwUU#*G`_ z$dAD}IXOxXm`Q;3s=N@kI!rUJAG zytpf@9v1z%C$0M0vSkbI#?evO9^tD(`HvL1th&O5wb{CL>xXccxQOw>-a_>soI7Pd zPUm<2<5IV}XhKsirz?p(?MIOP{kh~wRPz^l0O(3_-$ynNOVK=>Hf`Ei*nGf%0U%I? z%6FsG?$M(MGVq%?6YtBD&sN=3lf(}X2V*I_AgP+{hv_& z$_P4{r?z~zAaMpDIKFDX^Ugc(fX%yf>4Fr-qwpm~3uFEI^~1ek+DAu6&lX9Wvb`}0 zjO*@*Lx<~Od%*#(1TL&WNJvOae(Vq%8@t9T zF9aZovx-DWU_qpU&HKpy>SW3Rnb>0(iQqQ0`=q$1}B~9j5Cl0Cb}G zxP}JleX#SsYSk(np6?)RjgzTfg)b>uGR?UVlIgZ%#*BH4D}Z7vN_q(;4aTbhWJ*e- z{1rPWZ^~NA88)75cfLoq?r&(&HcPv_rfoyux%@dwar?`YDet?DoZswldK-&Kqle1v z3IjCnbNKM#lDFP^3!Ue-UAuPmI(gO$1_2Wrwr}4aUoHzhc<|s#;Yuy>LikiDPpT`5 ztQ?ITj^uQo?539o=t6bL4oL5VgY(OlE!za!3LDGAV@e1FK_XSJZQHi+Lhass^UVkI z^777E>4mU3OOom;2I#;F@(5_&=fHsj=Z6m;{v>SMrcE1oAKb#ltK``z(hD`}+_`fb zehiI>h?pbXswG|su&gqxBCw+3l91j9f8u-o{Q0Q7xK;|qA63C7x{~DCC|XG`7}~Bs zOIli5CMKe~JFSObgtJP3;v0s$g7&rL$jC@r-@biu^Qd;MTeofw8w*=27pv%`x3jD|^CMu5SgV!fkzMDcq$HX!VZs-%rPba?ik1s8ZfMY; z!7cpQH6|uzhX`fG#)+zi*Wc)<0F32K24b4DN}&3yj(HK*WJQzE1t9qQ7Z-*N8-~QWgH@GRSptTl zN6>^x6wHrZmM>raCEDOtY_z%pVmI=l>JrkB?^u;A>mj^PX+lB*O`bgY|6n^|L+mub zrj~jiDbfpR_Yu1O`s+jZvHPxFyM7U0EwQXBGXkGjbw@#b56s<1UguKPg%A3Q6)PfO zGhsWlW?)O7cpoX366)s7o5THe?A5E+z}VQ>3=zY9Mj|K%2dkvbTVutY=wTF+%WaI_mOfblre5<(xeF{ z)^o##4a;(JaxELpKt2AOb8Sy@#EaR3>*PMkPF^XAQ43)=`A;jB-kg)XDW zP}aC%!-j~d_$o-Ra2A~I#{!HC^EF^VD=SlaU=?kCfC=gyt`(Xc|}%2x@aJHWv;gcRf>-t?64 zUW|dURNWav1!S`j71!$3s}Hd)@TZ!(2-~1xD7Pyr-TO$<=R?g}wrtsfAG?Nzh7R1b zXHNo@?QbNKz?@)w34|PY5CByRApeiw>0j%w>w&Hd3JP#@%;XLoIt+m=giTPgS9;!4 zmdJbv9nIzY15|E{{8#^0XAS;Ag#bgDQx2xc~4n_ ziJ_y31g(Xv%s`^Z$jEqq$%(2XWX@x^+u?bV1bJk|g)c?GCdy73S}Ag?L;E_AV1M_H z9Xk&G<(FUfI#(V&?&ytwPMAJn2?YV+|vi-f$mXp!4^K-s!$=4M9B7A zwrSI*Ge7pITet45>({T}nwFNP^@&(y(=h-KARD&m^Q5Gt(2^xfcCe0bhpvUraX20} zkmT`u?;}N4W?FgAXw|%Vb1bMkFR(9{l|SMv3QApkasni(IAq9T;IHY>+_`gCv2Oc8 z$3oXQEuk;hs|b;g?){00-$da$crilNglKhqN1WozW(~_Nzf^D zD|9S9Pn9K)6g`O$hg)2G?X@AznlV-bV%9Sg(;EW3TEEfZ% zzDvjw9gnfGvG@w|w&394yP?BdZn>o=bSiW!tF0>Sd5j`G5f+AC1UB++VG?!c$DYWG zCQX_&e&4=*XVE=HQpidRc%#xQ6J@kPA|oTujvqgMB6J4bi4P)Y?UQ)+XJWIaFh}WUvSHoE#Zbiq<-VZjr-@09Xrxd?#oII zx~vIA5^F?@L#wdT)6;3omMxj1N00tYR9U@*u7ZW`phKUZD6DLeB$CNB+;3a{M0IJ@ zsFCK4IIiC{Z{ED`_wL<$Iy*aCQ?AI5s-;KLd7NUza1<7M_Ut)5W5$d{ZQHiJ8#;k* zSVvu)>2 z&0h;uAP#cC{rmS9E?BVOr=dfKKJLQnKu6FObXKkN9d|`~Bkk{$UE{1I+SadMzw=Eu z-PD_(_l6RmefHUBmMmGaffMa>aO=nmkXTm5@TLz_D9I6~Gcz;M(>b?r;lho>h7B7o zh|?Q7fG(gDp&RH3x~h?0rz|mgBb?Pmd~XMr%}PX*tPhlmBy!fQS>JEpzC8ooY@GZ< zu7G44fMNWROv2kZC&=>z`_V+j76jHZbLPy24?XnIlY%&Xux1giaE1rEfKCEl=PIJl zjnL0Pbpe;%{<`a~>xRnX8sU*x!Gpj2^2^V;YTL`_7A=NIkApR0tyr^IJ9GeD)Yx37q9VN!N>aFLB#Jl) zg|80<3y&m2TVKX)Pd@qNuqjif%-FbbV@z~(^aWIJ@JukIkx&pxDsOWr{TO0Q41Wb- z;2q&p16v#Iggs{O)}B3k-v92q@4h{K`t+I0moE=zN#byK3Wi0< zL&YP%EGqsIfn(_l`0F-Oc5 zbH?1o8n70uDd2G|6g`oU51?(2CK@UexP0sdb(Cb%d8SK*V%&M(efK@~&O7hCHFoUS zakFR7p1*kU;`M9SuHC(5%a)@%cI-I5d-v`PE~D*ChI|e=j>Cr!J7QvD@|k${Uw{2I zBO)RqW$V_hN7t=exBG`5eprt&#*G^{7Gq-UI(6y{6xY%SBR!3dG#$lUF=xyj$uZV~ zH3f*{1Cc~RLWzn*dL)p8`r?K#5Hkk%mH)J zlg8+60>r6gDT_=94QRZfbBp8@XJwG6;>&gL2uNIEY~&&xVSIQD5J8~3fcAg3(^z_oBqTpM!;kfsWWo=gZ0FdD)Hl2bI|&{KgYK>j3&1rLGH0^=j0 zh1UQX^uW*XJG=(3#d{=CFc!wd*tiC+g=^y4`tTGWO%)V9q4cDZ#DZsl!Qmxfd;ng< z73A;nTD%AEl|<2#B|w;JF4DM0a_Ipw=wbZ5o+LGvEdLL`ra(<9f-cMe0000 + + diff --git a/play-services-location/core/src/huawei/res/drawable/permission_step_1.jpg b/play-services-location/core/src/huawei/res/drawable/permission_step_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dbd4cc6bc065d6bd5b8bc6d482bb50132fc8a8b8 GIT binary patch literal 41041 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 PNV~{f?{~HTd-4AQUGSTU literal 0 HcmV?d00001 diff --git a/play-services-location/core/src/huawei/res/drawable/permission_step_2.jpg b/play-services-location/core/src/huawei/res/drawable/permission_step_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ba1576b184471be2eff9f007ba8938a92b0427d9 GIT binary patch literal 57346 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 + + + + + + + + + + + + + + + + + + + + + + + + + + +