chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
+2
View File
@@ -0,0 +1,2 @@
// @generated by expo-module-scripts
module.exports = require('expo-module-scripts/eslintrc.base.js');
+523
View File
@@ -0,0 +1,523 @@
# Changelog
## Unpublished
### 🛠 Breaking changes
### 🎉 New features
### 🐛 Bug fixes
### 💡 Others
## 19.0.8 — 2025-12-05
_This version does not introduce any user-facing changes._
## 19.0.7 — 2025-09-11
_This version does not introduce any user-facing changes._
## 19.0.6 — 2025-09-02
_This version does not introduce any user-facing changes._
## 19.0.5 — 2025-08-31
_This version does not introduce any user-facing changes._
## 19.0.4 — 2025-08-27
_This version does not introduce any user-facing changes._
## 19.0.3 — 2025-08-25
_This version does not introduce any user-facing changes._
## 19.0.2 — 2025-08-16
_This version does not introduce any user-facing changes._
## 19.0.1 — 2025-08-15
_This version does not introduce any user-facing changes._
## 19.0.0 — 2025-08-13
_This version does not introduce any user-facing changes._
## 18.1.6 - 2025-07-01
### 💡 Others
- Remove "Please" from warnings and errors ([#36862](https://github.com/expo/expo/pull/36862) by [@brentvatne](https://github.com/brentvatne))
## 18.1.5 — 2025-05-08
### 🐛 Bug fixes
- [Android] Fix `shouldUseForegroundService` being always `true`. ([#35875](https://github.com/expo/expo/pull/35875) by [@filipef101](https://github.com/filipef101))
## 18.1.4 — 2025-04-30
_This version does not introduce any user-facing changes._
## 18.1.3 — 2025-04-25
_This version does not introduce any user-facing changes._
## 18.1.2 — 2025-04-14
_This version does not introduce any user-facing changes._
## 18.1.1 — 2025-04-09
_This version does not introduce any user-facing changes._
## 18.1.0 — 2025-04-04
### 🐛 Bug fixes
- [Android] Add missing ProGuard rule to fix task consumer failed ([#34098](https://github.com/expo/expo/pull/34098) by [@cornejobarraza](https://github.com/cornejobarraza))
- [iOS] `startLocationUpdatesAsync` should not require background permissions ([#33617](https://github.com/expo/expo/pull/33617) by [@andrejpavlovic](https://github.com/andrejpavlovic)
### 💡 Others
- On Android, remove dependency on `smart-location-lib`. ([#33609](https://github.com/expo/expo/pull/33609) by [@alanjhughes](https://github.com/alanjhughes))
- [Android] Started using expo modules gradle plugin. ([#34176](https://github.com/expo/expo/pull/34176) by [@lukmccall](https://github.com/lukmccall))
- [Android] Added missing dependency. ([#35822](https://github.com/expo/expo/pull/35822) by [@lukmccall](https://github.com/lukmccall))
## 18.0.10 - 2025-04-01
### 🐛 Bug fixes
- [iOS] Fixed issue with some permission request flows resolving too soon on iOS. ([#35693](https://github.com/expo/expo/pull/35693) by [@chrfalch](https://github.com/chrfalch))
- [iOS] Remove restarting all services when CLLocationManager reports an error ([#35478](https://github.com/expo/expo/pull/35478) by [@chrfalch](https://github.com/chrfalch))
## 18.0.9 - 2025-03-31
_This version does not introduce any user-facing changes._
## 18.0.8 - 2025-03-14
### 💡 Others
- On iOS, added setting the scope value as per our documentation. ([#35452](https://github.com/expo/expo/pull/35452) by [@chrfalch](https://github.com/chrfalch))
## 18.0.7 - 2025-02-19
### 🐛 Bug fixes
- [iOS] Added guards to avoid task options to crash the app. ([#35477](https://github.com/expo/expo/pull/35477) by [@chrfalch](https://github.com/chrfalch))
- [iOS] Added error handler to the streaming location/heading methods since these can fail while streaming ([#35004](https://github.com/expo/expo/pull/35004) by [@chrfalch](https://github.com/chrfalch))
## 18.0.6 - 2025-02-10
### 🐛 Bug fixes
- [Android] Use less specific exception in catch block of `resolveUserSettingsForRequest`. ([#34784](https://github.com/expo/expo/pull/34784) by [@alanjhughes](https://github.com/alanjhughes))
## 18.0.5 - 2025-01-10
_This version does not introduce any user-facing changes._
## 18.0.4 - 2024-12-10
_This version does not introduce any user-facing changes._
## 18.0.3 - 2024-11-29
_This version does not introduce any user-facing changes._
## 18.0.2 — 2024-11-19
### 🐛 Bug fixes
- Fixed NoClassDefFoundError for `Landroid/support/v4/app/ActivityCompat;`. ([#33088](https://github.com/expo/expo/pull/33088) by [@kudo](https://github.com/kudo))
## 18.0.1 — 2024-10-22
_This version does not introduce any user-facing changes._
## 18.0.0 — 2024-10-22
### 🛠 Breaking changes
- Remove deprecated code: geocoding using Google Maps API, old permission methods and related types. ([#29961](https://github.com/expo/expo/pull/29961) by [@Simek](https://github.com/Simek))
- Bumped iOS deployment target to 15.1. ([#30840](https://github.com/expo/expo/pull/30840), [#30867](https://github.com/expo/expo/pull/30867) by [@tsapeta](https://github.com/tsapeta))
### 🎉 New features
- [iOS] Migrated the native module to Swift. ([#30388](https://github.com/expo/expo/pull/30388) by [@tsapeta](https://github.com/tsapeta))
### 🐛 Bug fixes
- On `iOS`, fix an issue where if the user selects "Allow Once" for location permissions, we needed to request background permissions twice because the first time had effect. ([#29272](https://github.com/expo/expo/pull/29272) by [@alanjhughes](https://github.com/alanjhughes))
### 💡 Others
- Added warning when using background location in Expo Go. ([#31461](https://github.com/expo/expo/pull/31461) by [@chrfalch](https://github.com/chrfalch))
- Keep using the legacy event emitter as the module is not fully migrated to Expo Modules API. ([#28946](https://github.com/expo/expo/pull/28946) by [@tsapeta](https://github.com/tsapeta))
- Rework package exports. ([#29953](https://github.com/expo/expo/pull/29953) by [@Simek](https://github.com/Simek))
- Updated documentation for default LocationAccuracy ([#31066](https://github.com/expo/expo/pull/31066) by [@johnculviner](https://github.com/johnculviner))
## 17.0.1 — 2024-04-23
_This version does not introduce any user-facing changes._
## 17.0.0 — 2024-04-18
### 🛠 Breaking changes
- [Web] `getPermissionsAsync` no longer prompts the user for permission instead we use the new browser API `navigator.permissions.query` to check the permission status. ([#26836](https://github.com/expo/expo/pull/26837) by [@hems](https://github.com/hems))
### 🎉 New features
- Add ability to disable permissions in config plugin by passing `false` instead of permission messages. ([#28107](https://github.com/expo/expo/pull/28107) by [@EvanBacon](https://github.com/EvanBacon))
### 💡 Others
- drop unused web `name` property. ([#27437](https://github.com/expo/expo/pull/27437) by [@EvanBacon](https://github.com/EvanBacon))
- Removed deprecated backward compatible Gradle settings. ([#28083](https://github.com/expo/expo/pull/28083) by [@kudo](https://github.com/kudo))
- Fixed unit test errors. ([#28208](https://github.com/expo/expo/pull/28208) by [@kudo](https://github.com/kudo))
## 16.5.5 - 2024-02-29
_This version does not introduce any user-facing changes._
## 16.5.4 - 2024-02-27
### 🎉 New features
- [Android] Make foreground service permission opt-in with `isAndroidForegroundServiceEnabled` config plugin option [#27265](https://github.com/expo/expo/pull/27265) by [@brentvatne](https://github.com/brentvatne))
- [Android] Enable foreground service by default when background location is enabled [#27359](https://github.com/expo/expo/pull/27359) by [@brentvatne](https://github.com/brentvatne))
## 16.5.3 - 2024-02-06
### 🐛 Bug fixes
- [Android] Fixed: `NullPointerException: it must not be null`. ([#26688](https://github.com/expo/expo/pull/26688) by [@lukmccall](https://github.com/lukmccall))
- On `Android`, prevent location service from starting when permission is not in the manifest. ([#27355](https://github.com/expo/expo/pull/27355) by [@alanjhughes](https://github.com/alanjhughes))
## 16.5.2 - 2024-01-10
### 🎉 New features
- [Android] Added `formattedAddress` to the `LocationGeocodedAddress`. ([#26342](https://github.com/expo/expo/pull/26342) by [@whysetiawan](https://github.com/whysetiawan) & [@lukmccall](https://github.com/lukmccall)) ([#26342](https://github.com/expo/expo/pull/26342) by [@whysetiawan](https://github.com/whysetiawan), [@lukmccall](https://github.com/lukmccall))
### 🐛 Bug fixes
- [Android] Fix the module requiring the `expo-task-manager` module for methods that don't use it. ([#26200](https://github.com/expo/expo/pull/26200) by [@behenate](https://github.com/behenate))
## 16.5.1 - 2023-12-19
_This version does not introduce any user-facing changes._
## 16.5.0 — 2023-11-14
### 🛠 Breaking changes
- Bumped iOS deployment target to 13.4. ([#25063](https://github.com/expo/expo/pull/25063) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- On `Android` bump `compileSdkVersion` and `targetSdkVersion` to `34`. ([#24708](https://github.com/expo/expo/pull/24708) by [@alanjhughes](https://github.com/alanjhughes))
### 💡 Others
- [Android] Moved to the new Modules API. ([#24737](https://github.com/expo/expo/pull/24737) by [@behenate](https://github.com/behenate))
- Remove `unimodule.json` in favour of `expo-module.config.json`. ([#25100](https://github.com/expo/expo/pull/25100) by [@reichhartd](https://github.com/reichhartd))
### 📚 3rd party library updates
- Updated `com.google.android.gms:play-services-location` to `21.0.1`. ([#25028](https://github.com/expo/expo/pull/25028) by [@behenate](https://github.com/behenate))
## 16.4.0 — 2023-10-17
### 🛠 Breaking changes
- Dropped support for Android SDK 21 and 22. ([#24201](https://github.com/expo/expo/pull/24201) by [@behenate](https://github.com/behenate))
## 16.3.0 — 2023-09-04
### 🎉 New features
- Added support for React Native 0.73. ([#24018](https://github.com/expo/expo/pull/24018) by [@kudo](https://github.com/kudo))
## 16.2.1 — 2023-08-02
_This version does not introduce any user-facing changes._
## 16.2.0 — 2023-07-28
_This version does not introduce any user-facing changes._
## 16.1.0 - 2023-07-13
### 🐛 Bug fixes
- Downgrade play-services-location to 20.0.0 to support react-native-maps. ([#23501](https://github.com/expo/expo/pull/23501) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## 16.0.0 — 2023-06-21
_This version does not introduce any user-facing changes._
## 15.3.0 — 2023-06-13
### 📚 3rd party library updates
- Updated `com.google.android.gms:play-services-location` to `21.0.1` and `io.nlopez.smartlocation:library` to `3.3.3` ([#22468](https://github.com/expo/expo/pull/22468) by [@josephyanks](https://github.com/josephyanks))
### 🐛 Bug fixes
- Fixed Android build warnings for Gradle version 8. ([#22537](https://github.com/expo/expo/pull/22537), [#22609](https://github.com/expo/expo/pull/22609) by [@kudo](https://github.com/kudo))
### 💡 Others
- On Android, removed use of deprecated `LocationRequest` constructor and replaced with `LocationRequest.Builder`. ([#22653](https://github.com/expo/expo/pull/22653) by [@alanjhughes](https://github.com/alanjhughes))
- Removed the Geocoding API service. ([#22830](https://github.com/expo/expo/pull/22830) by [@alanjhughes](https://github.com/alanjhughes))
## 15.2.0 — 2023-05-08
_This version does not introduce any user-facing changes._
## 15.1.1 — 2023-02-09
_This version does not introduce any user-facing changes._
## 15.1.0 — 2023-02-03
### 🐛 Bug fixes
- Removed strict null checks for expo location and avoid crash on android. ([#20792](https://github.com/expo/expo/pull/20792) by [@jayshah123](https://github.com/jayshah123) and [@forki](https://github.com/forki))
- Export types with type-only annotation to fix build when using `isolatedModules` flag. ([#20239](https://github.com/expo/expo/pull/20239) by [@zakharchenkoAndrii](https://github.com/zakharchenkoAndrii))
### 💡 Others
- On Android bump `compileSdkVersion` and `targetSdkVersion` to `33`. ([#20721](https://github.com/expo/expo/pull/20721) by [@lukmccall](https://github.com/lukmccall))
## 15.0.1 — 2022-10-28
_This version does not introduce any user-facing changes._
## 15.0.0 — 2022-10-25
### 🛠 Breaking changes
- Bumped iOS deployment target to 13.0 and deprecated support for iOS 12. ([#18873](https://github.com/expo/expo/pull/18873) by [@tsapeta](https://github.com/tsapeta))
### 🐛 Bug fixes
- Fixed `trueHeading` is sometimes bigger then 360 on Android. ([#19629](https://github.com/expo/expo/pull/19629) by [@lukmccall](https://github.com/lukmccall))
### 💡 Others
- [plugin] Migrate import from @expo/config-plugins to expo/config-plugins and @expo/config-types to expo/config. ([#18855](https://github.com/expo/expo/pull/18855) by [@brentvatne](https://github.com/brentvatne))
- Drop `@expo/config-plugins` dependency in favor of peer dependency on `expo`. ([#18595](https://github.com/expo/expo/pull/18595) by [@EvanBacon](https://github.com/EvanBacon))
## 14.3.0 — 2022-07-07
### 🐛 Bug fixes
- Fixed Android 12+ runtime crash caused by `PendingIntent` misconfiguration. ([#17333](https://github.com/expo/expo/pull/17333) by [@kudo](https://github.com/kudo))
## 14.2.1 — 2022-04-20
_This version does not introduce any user-facing changes._
## 14.2.0 — 2022-04-18
### 🐛 Bug fixes
- Fix `Plugin with id 'maven' not found` build error from Android Gradle 7. ([#16080](https://github.com/expo/expo/pull/16080) by [@kudo](https://github.com/kudo))
- Fix LocationObject type ([#17070](https://github.com/expo/expo/pull/17070) by [@rakeshpetit](https://github.com/rakeshpetit))
### ⚠️ Notices
- On Android bump `compileSdkVersion` to `31`, `targetSdkVersion` to `31` and `Java` version to `11`. ([#16941](https://github.com/expo/expo/pull/16941) by [@bbarthec](https://github.com/bbarthec))
## 14.1.0 — 2022-01-26
### 🐛 Bug fixes
- Allow location to work on Android with only coarse location permission. All apps do not require fine/precise location permission, but in past Expo was enforcing fine/precise even if you only needed coarse level location. ([#15760](https://github.com/expo/expo/pull/15760) by [@Noitidart](https://github.com/Noitidart))
## Unpublished
### 🛠 Breaking changes
- Add an option to whether kill or keep the foreground service when app is killed on Android. ([#15633](https://github.com/expo/expo/pull/15633) by [@islamouzou](https://github.com/islamouzou))
- Updated `@expo/config-plugins` from `4.0.2` to `4.0.14` ([#15621](https://github.com/expo/expo/pull/15621) by [@EvanBacon](https://github.com/EvanBacon))
## 14.0.2 — 2022-02-01
### 🐛 Bug fixes
- Fix `Plugin with id 'maven' not found` build error from Android Gradle 7. ([#16080](https://github.com/expo/expo/pull/16080) by [@kudo](https://github.com/kudo))
## 14.0.1 — 2021-12-15
_This version does not introduce any user-facing changes._
## 14.0.0 — 2021-12-03
### 🛠 Breaking changes
- Remove deprecated `setApiKey` method. ([#14672](https://github.com/expo/expo/pull/14672) by [@Simek](https://github.com/Simek))
### 🎉 New features
- Added steetNumber to `reverseGeocodeAsync` for iOS ([#13556](https://github.com/expo/expo/pull/13556) by [@chrisdrackett](https://github.com/chrisdrackett))
### 🐛 Bug fixes
- Call `jobService.jobFinished` for the finished geofencing jobs. ([#14786](https://github.com/expo/expo/pull/14786) by [@mdmitry01](https://github.com/mdmitry01))
- Check for null value of `mLocationClient` to prevent a crash ([#15023](https://github.com/expo/expo/pull/15023) by [@zakharchenkoAndrii](https://github.com/zakharchenkoAndrii))
### 💡 Others
- Extract nested `foregroundService` object from `LocationTaskOptions` type to the separate type `LocationTaskServiceOptions`. ([#14672](https://github.com/expo/expo/pull/14672) by [@Simek](https://github.com/Simek))
## 13.0.1 — 2021-10-01
_This version does not introduce any user-facing changes._
## 13.0.0 — 2021-09-28
### 🛠 Breaking changes
- Dropped support for iOS 11.0 ([#14383](https://github.com/expo/expo/pull/14383) by [@cruzach](https://github.com/cruzach))
### 🎉 New features
- [plugin] Added `isIosBackgroundLocationEnabled` to enable the background location UIMode ([#14142](https://github.com/expo/expo/pull/14142) by [@EvanBacon](https://github.com/EvanBacon))
- Use stable manifest ID where applicable. ([#12964](https://github.com/expo/expo/pull/12964) by [@wschurman](https://github.com/wschurman))
- Add useForegroundPermissions and useBackgroundPermissions hooks from modules factory. ([#13860](https://github.com/expo/expo/pull/13860) by [@bycedric](https://github.com/bycedric))
### 🐛 Bug fixes
- Fixed `Location.getCurrentPositionAsync` throwing `Location provider is unavailable.` error. ([#14281](https://github.com/expo/expo/pull/14281) by [@m1st4ke](https://github.com/m1st4ke))
- Fix building errors from use_frameworks! in Podfile. ([#14523](https://github.com/expo/expo/pull/14523) by [@kudo](https://github.com/kudo))
- Update error message for `LocationUnavailableException` on Android. ([#14539](https://github.com/expo/expo/pull/14539) by [@kylerjensen](https://github.com/kylerjensen))
### 💡 Others
- Updated `@expo/config-plugins` ([#14443](https://github.com/expo/expo/pull/14443) by [@EvanBacon](https://github.com/EvanBacon))
## 12.1.0 — 2021-06-16
### 🐛 Bug fixes
- Fixed `startLocationUpdatesAsync` requiring the background location permission even if was used when the app is in the foreground on iOS. ([#12594](https://github.com/expo/expo/pull/12594) by [@lukmccall](https://github.com/lukmccall))
- Enable kotlin in all modules. ([#12716](https://github.com/expo/expo/pull/12716) by [@wschurman](https://github.com/wschurman))
### 💡 Others
- Migrated from `unimodules-permissions-interface` to `expo-modules-core`. ([#12961](https://github.com/expo/expo/pull/12961) by [@tsapeta](https://github.com/tsapeta))
## 12.0.4 — 2021-04-13
_This version does not introduce any user-facing changes._
## 12.0.3 — 2021-04-09
### 🐛 Bug fixes
- Add support for user-initiated background tracking without background permission ([#12456](https://github.com/expo/expo/pull/12456) by [@bycedric](https://github.com/bycedric))
## 12.0.2 — 2021-03-29
### 🐛 Bug fixes
- Lock the unimodules-permissions-interface dependency to the same version in react-native-unimodules
## 12.0.1 — 2021-03-26
### 🐛 Bug fixes
- Add missing unimodules-permissions-interface dependency
## 12.0.0 — 2021-03-10
### 🛠 Breaking changes
- Splitting location permissions into `Foreground` and `Background` permissions. ([#12063](https://github.com/expo/expo/pull/12063) by [@lukmccall](https://github.com/lukmccall))
### 🎉 New features
- Converted plugin to TypeScript. ([#11715](https://github.com/expo/expo/pull/11715) by [@EvanBacon](https://github.com/EvanBacon))
- Updated Android build configuration to target Android 11 (added support for Android SDK 30). ([#11647](https://github.com/expo/expo/pull/11647) by [@bbarthec](https://github.com/bbarthec))
### 🐛 Bug fixes
- Remove sticky notification on service stop on Android. ([#11775](https://github.com/expo/expo/pull/11775) by [@zaguiini](https://github.com/zaguiini))
- Remove peerDependencies and unimodulePeerDependencies from Expo modules. ([#11980](https://github.com/expo/expo/pull/11980) by [@brentvatne](https://github.com/brentvatne))
## 11.0.0 — 2021-01-15
### ⚠️ Notices
- The package is now shipped with prebuilt binaries on iOS. You can read more about it on [expo.fyi/prebuilt-modules](https://expo.fyi/prebuilt-modules). ([#11224](https://github.com/expo/expo/pull/11224) by [@tsapeta](https://github.com/tsapeta))
### 🛠 Breaking changes
- Dropped support for iOS 10.0 ([#11344](https://github.com/expo/expo/pull/11344) by [@tsapeta](https://github.com/tsapeta))
### 🎉 New features
- Created config plugins ([#11538](https://github.com/expo/expo/pull/11538) by [@EvanBacon](https://github.com/EvanBacon))
### 🐛 Bug fixes
- Fixed background location permission check on Android. ([#11399](https://github.com/expo/expo/pull/11399) by [@peterdn](https://github.com/peterdn))
## 10.0.0 — 2020-11-17
### 🛠 Breaking changes
- Make background location an opt-in permission on Android. ([#10989](https://github.com/expo/expo/pull/10989) by [@bycedric](https://github.com/bycedric))
## 9.0.1 — 2020-10-02
### 🐛 Bug fixes
- Redeliver intent when restarting task service. ([#10410](https://github.com/expo/expo/pull/10410) by [@byCedric](https://github.com/byCedric))
## 9.0.0 — 2020-08-18
### 🛠 Breaking changes
- Add `scope` field in returned value to indicate whether background permissions are granted. Add `android.accuracy` field to determine whether `coarse` or `fine` location permission is granted. ([#9446](https://github.com/expo/expo/pull/9446) by [@mczernek](https://github.com/mczernek))
- `getLastKnownPositionAsync` no longer rejects when the last known location is not available now it returns `null`. ([#9251](https://github.com/expo/expo/pull/9251) by [@tsapeta](https://github.com/tsapeta))
- Removed the deprecated `enableHighAccuracy` option of `getCurrentPositionAsync`. ([#9251](https://github.com/expo/expo/pull/9251) by [@tsapeta](https://github.com/tsapeta))
- Removed `maximumAge` and `timeout` options from `getCurrentPositionAsync` it's been Android only and the same behavior can be achieved on all platforms on the JavaScript side. ([#9251](https://github.com/expo/expo/pull/9251) by [@tsapeta](https://github.com/tsapeta))
- Made type and enum names more consistent and in line with our standards — they all are now prefixed by `Location`. The most common ones are still accessible without the prefix, but it's not the recommended way. ([#9251](https://github.com/expo/expo/pull/9251) by [@tsapeta](https://github.com/tsapeta))
- `geocodeAsync` and `reverseGeocodeAsync` no longer falls back to Google Maps API on Android. ([#9444](https://github.com/expo/expo/pull/9444) by [@tsapeta](https://github.com/tsapeta))
### 🎉 New features
- Added missing `altitudeAccuracy` to the location object on Android (requires at least Android 8.0). ([#9251](https://github.com/expo/expo/pull/9251) by [@tsapeta](https://github.com/tsapeta))
- Improved support for Web — added missing methods for requesting permissions and getting last known position. ([#9251](https://github.com/expo/expo/pull/9251) by [@tsapeta](https://github.com/tsapeta))
- Added `maxAge` and `requiredAccuracy` options to `getLastKnownPositionAsync`. ([#9251](https://github.com/expo/expo/pull/9251) by [@tsapeta](https://github.com/tsapeta))
- Google Maps Geocoding API can now be used on all platforms with the new `useGoogleMaps` option. ([#9444](https://github.com/expo/expo/pull/9444) by [@tsapeta](https://github.com/tsapeta))
- Added `district`, `subregion` and `timezone` values to reverse-geocoded address object. ([#9444](https://github.com/expo/expo/pull/9444) by [@tsapeta](https://github.com/tsapeta))
### 🐛 Bug fixes
- Fixed different types being used on Web platform. ([#9251](https://github.com/expo/expo/pull/9251) by [@tsapeta](https://github.com/tsapeta))
- `getLastKnownPositionAsync` no longer requests for the current location on iOS and just returns the last known one as it should be. ([#9251](https://github.com/expo/expo/pull/9251) by [@tsapeta](https://github.com/tsapeta))
- Fixed `getCurrentPositionAsync` not resolving on Android when the lowest accuracy is used. ([#9251](https://github.com/expo/expo/pull/9251) by [@tsapeta](https://github.com/tsapeta))
- Fixed `LocationGeocodedAddress` type to reflect the possibility of receiving `null` values. ([#9444](https://github.com/expo/expo/pull/9444) by [@tsapeta](https://github.com/tsapeta))
## 8.3.0 — 2020-07-16
### 🐛 Bug fixes
- Added some safety checks to prevent `NullPointerExceptions` in background location on Android. ([#8864](https://github.com/expo/expo/pull/8864) by [@mczernek](https://github.com/mczernek))
- Add `isoCountryCode` to `Address` type and reverse lookup. ([#8913](https://github.com/expo/expo/pull/8913) by [@bycedric](https://github.com/bycedric))
- Fix geocoding requests not resolving/rejecting on iOS when the app is in the background or inactive state. It makes it possible to use geocoding in such app states, however it's still discouraged. ([#9178](https://github.com/expo/expo/pull/9178) by [@tsapeta](https://github.com/tsapeta))
## 8.2.1 — 2020-05-29
_This version does not introduce any user-facing changes._
## 8.2.0 — 2020-05-27
_This version does not introduce any user-facing changes._
+66
View File
@@ -0,0 +1,66 @@
<p>
<a href="https://docs.expo.dev/versions/latest/sdk/location/">
<img
src="../../.github/resources/expo-location.svg"
alt="expo-location"
height="64" />
</a>
</p>
Allows reading geolocation information from the device. Your app can poll for the current location or subscribe to location update events.
# API documentation
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/location/)
- [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/location/)
# Installation in managed Expo projects
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/location/).
# Installation in bare React Native projects
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
### Add the package to your npm dependencies
```
npx expo install expo-location
```
### Configure for Android
This module requires the permissions for approximate and exact device location. It also needs the foreground service permission to subscribe to location updates, while the app is in use. These permissions are automatically added.
```xml
<!-- Added permissions -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Optional permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
```
> [!note]
> On Android, you have to [submit your app for review and request access to use the background location permission](https://support.google.com/googleplay/android-developer/answer/9799150?hl=en) or [foreground location permissions](https://support.google.com/googleplay/android-developer/answer/13392821?hl=en).
### Configure for iOS
Add `NSLocationAlwaysAndWhenInUseUsageDescription`, `NSLocationAlwaysUsageDescription` and `NSLocationWhenInUseUsageDescription` keys to your `Info.plist`:
```xml
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to use your location</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to use your location</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to use your location</string>
```
Run `npx pod-install` after installing the npm package.
# Contributing
Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).
+21
View File
@@ -0,0 +1,21 @@
plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'host.exp.exponent'
version = '19.0.8'
android {
namespace "expo.modules.location"
defaultConfig {
versionCode 29
versionName "19.0.8"
consumerProguardFiles("proguard-rules.pro")
}
}
dependencies {
api 'com.google.android.gms:play-services-location:21.0.1'
implementation 'androidx.annotation:annotation:1.7.1'
}
+3
View File
@@ -0,0 +1,3 @@
# https://github.com/expo/expo/issues/3918
-keep class expo.modules.location.taskConsumers.** { *; }
@@ -0,0 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application>
<service
android:name=".services.LocationTaskService"
android:exported="false"
android:foregroundServiceType="location" />
</application>
</manifest>
@@ -0,0 +1,5 @@
package expo.modules.location
interface LocationActivityResultListener {
fun onResult(resultCode: Int)
}
@@ -0,0 +1,60 @@
package expo.modules.location
import expo.modules.kotlin.exception.CodedException
internal class NoPermissionsModuleException :
CodedException("Permissions module is null. Are you sure all the installed Expo modules are properly linked?")
internal class NoPermissionInManifestException(permissionName: String) :
CodedException("You need to add `$permissionName` to the AndroidManifest")
internal class LocationBackgroundUnauthorizedException :
CodedException("Not authorized to use background location services")
internal class LocationRequestRejectedException(cause: Exception) :
CodedException("Location request has been rejected: " + cause.message)
internal class CurrentLocationIsUnavailableException :
CodedException("Current location is unavailable. Make sure that location services are enabled")
internal class LocationRequestCancelledException :
CodedException("Location request has been cancelled")
internal class LocationSettingsUnsatisfiedException :
CodedException("Location request failed due to unsatisfied device settings")
internal class LocationUnauthorizedException :
CodedException("Not authorized to use location services")
internal class LocationUnavailableException :
CodedException("Location is unavailable. Make sure that location services are enabled")
internal class LocationUnknownException :
CodedException("Current location is unknown")
internal class SensorManagerUnavailable :
CodedException("Sensor manager is unavailable")
internal class GeocodeException(message: String?, cause: Throwable? = null) :
CodedException("An exception occurred when accessing the geocode: ${message ?: ""} ${cause?.message ?: ""}")
internal class NoGeocodeException :
CodedException("Could not find the Geocoder")
internal class TaskManagerNotFoundException :
CodedException("Could not find the task manager")
internal class GeofencingException(message: String?, cause: Throwable? = null) :
CodedException("A geofencing exception has occurred: ${message ?: ""} ${cause?.message ?: ""}")
internal class MissingUIManagerException :
CodedException("UIManager is unavailable")
internal class ConversionException(fromClass: Class<*>, toClass: Class<*>, message: String? = "") :
CodedException("Couldn't cast from ${fromClass::class.simpleName} to ${toClass::class.java.simpleName}: $message")
internal class ForegroundServiceStartNotAllowedException :
CodedException("Couldn't start the foreground service. Foreground service cannot be started when the application is in the background")
internal class ForegroundServicePermissionsException :
CodedException("Couldn't start the foreground service. Foreground service permissions were not found in the manifest")
@@ -0,0 +1,201 @@
package expo.modules.location
import android.content.Context
import android.location.Location
import android.location.LocationManager
import android.os.Bundle
import com.google.android.gms.location.CurrentLocationRequest
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.Granularity
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.Priority
import expo.modules.interfaces.permissions.Permissions
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.CodedException
import expo.modules.location.records.LocationLastKnownOptions
import expo.modules.location.records.LocationOptions
import expo.modules.location.records.LocationResponse
import expo.modules.location.records.PermissionRequestResponse
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class LocationHelpers {
companion object {
/**
* Checks whether given location didn't exceed given `maxAge` and fits in the required accuracy.
*/
internal fun isLocationValid(location: Location?, options: LocationLastKnownOptions): Boolean {
if (location == null) {
return false
}
val maxAge = options.maxAge ?: Double.MAX_VALUE
val requiredAccuracy = options.requiredAccuracy ?: Double.MAX_VALUE
val timeDiff = (System.currentTimeMillis() - location.time).toDouble()
return timeDiff <= maxAge && location.accuracy <= requiredAccuracy
}
fun hasNetworkProviderEnabled(context: Context?): Boolean {
if (context == null) {
return false
}
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
return locationManager != null && locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
internal fun prepareLocationRequest(options: LocationOptions): LocationRequest {
val locationParams = mapOptionsToLocationParams(options)
return LocationRequest.Builder(locationParams.interval)
.setMinUpdateIntervalMillis(locationParams.interval)
.setMaxUpdateDelayMillis(locationParams.interval)
.setMinUpdateDistanceMeters(locationParams.distance)
.setPriority(mapAccuracyToPriority(options.accuracy))
.build()
}
internal fun prepareCurrentLocationRequest(options: LocationOptions): CurrentLocationRequest {
val locationParams = mapOptionsToLocationParams(options)
return CurrentLocationRequest.Builder().apply {
setGranularity(Granularity.GRANULARITY_PERMISSION_LEVEL)
setPriority(mapAccuracyToPriority(options.accuracy))
setMaxUpdateAgeMillis(locationParams.interval)
}.build()
}
fun requestSingleLocation(locationProvider: FusedLocationProviderClient, locationRequest: CurrentLocationRequest, promise: Promise) {
try {
locationProvider.getCurrentLocation(locationRequest, null)
.addOnSuccessListener { location: Location? ->
if (location == null) {
promise.reject(CurrentLocationIsUnavailableException())
return@addOnSuccessListener
}
promise.resolve(LocationResponse(location))
}
.addOnFailureListener {
promise.reject(LocationRequestRejectedException(it))
}
.addOnCanceledListener {
promise.reject(LocationRequestCancelledException())
}
} catch (e: SecurityException) {
promise.reject(LocationRequestRejectedException(e))
}
}
fun requestContinuousUpdates(locationModule: LocationModule, locationRequest: LocationRequest, watchId: Int, promise: Promise) {
locationModule.requestLocationUpdates(
locationRequest,
watchId,
object : LocationRequestCallbacks {
override fun onLocationChanged(location: Location) {
locationModule.sendLocationResponse(watchId, LocationResponse(location))
}
override fun onRequestSuccess() {
promise.resolve(null)
}
override fun onRequestFailed(cause: CodedException) {
promise.reject(cause)
}
}
)
}
private fun mapOptionsToLocationParams(options: LocationOptions): LocationParams {
val accuracy = options.accuracy
val locationParams = buildLocationParamsForAccuracy(accuracy)
options.timeInterval?.let {
locationParams.interval = it
}
options.distanceInterval?.let {
locationParams.distance = it.toFloat()
}
return locationParams
}
private fun mapAccuracyToPriority(accuracy: Int): Int {
return when (accuracy) {
LocationModule.ACCURACY_BEST_FOR_NAVIGATION, LocationModule.ACCURACY_HIGHEST, LocationModule.ACCURACY_HIGH -> Priority.PRIORITY_HIGH_ACCURACY
LocationModule.ACCURACY_BALANCED, LocationModule.ACCURACY_LOW -> Priority.PRIORITY_BALANCED_POWER_ACCURACY
LocationModule.ACCURACY_LOWEST -> Priority.PRIORITY_LOW_POWER
else -> Priority.PRIORITY_BALANCED_POWER_ACCURACY
}
}
private fun buildLocationParamsForAccuracy(accuracy: Int): LocationParams {
return when (accuracy) {
LocationModule.ACCURACY_LOWEST -> LocationParams(accuracy = LocationAccuracy.LOWEST, distance = 3000f, interval = 10000)
LocationModule.ACCURACY_LOW -> LocationParams(accuracy = LocationAccuracy.LOW, distance = 1000f, interval = 5000)
LocationModule.ACCURACY_BALANCED -> LocationParams(accuracy = LocationAccuracy.MEDIUM, distance = 100f, interval = 3000)
LocationModule.ACCURACY_HIGH -> LocationParams(accuracy = LocationAccuracy.HIGH, distance = 50f, interval = 2000)
LocationModule.ACCURACY_HIGHEST -> LocationParams(accuracy = LocationAccuracy.HIGH, distance = 25f, interval = 1000)
LocationModule.ACCURACY_BEST_FOR_NAVIGATION -> LocationParams(accuracy = LocationAccuracy.HIGH, distance = 0f, interval = 500)
else -> LocationParams(accuracy = LocationAccuracy.MEDIUM, distance = 100f, interval = 3000)
}
}
fun isAnyProviderAvailable(context: Context?): Boolean {
val locationManager = context?.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
?: return false
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
// Decorator for Permissions.getPermissionsWithPermissionsManager, for use in Kotlin coroutines
internal suspend fun getPermissionsWithPermissionsManager(contextPermissions: Permissions, vararg permissionStrings: String): PermissionRequestResponse {
return suspendCoroutine { continuation ->
Permissions.getPermissionsWithPermissionsManager(
contextPermissions,
object : Promise {
override fun resolve(value: Any?) {
val result = value as? Bundle
?: throw ConversionException(Any::class.java, Bundle::class.java, "value returned by the permission promise is not a Bundle")
continuation.resume(PermissionRequestResponse(result))
}
override fun reject(code: String, message: String?, cause: Throwable?) {
continuation.resumeWithException(CodedException(code, message, cause))
}
},
*permissionStrings
)
}
}
// Decorator for Permissions.getPermissionsWithPermissionsManager, for use in Kotlin coroutines
internal suspend fun askForPermissionsWithPermissionsManager(contextPermissions: Permissions, vararg permissionStrings: String): Bundle {
return suspendCoroutine {
Permissions.askForPermissionsWithPermissionsManager(
contextPermissions,
object : Promise {
override fun resolve(value: Any?) {
it.resume(
value as? Bundle
?: throw ConversionException(Any::class.java, Bundle::class.java, "value returned by the permission promise is not a Bundle")
)
}
override fun reject(code: String, message: String?, cause: Throwable?) {
it.resumeWithException(CodedException(code, message, cause))
}
},
*permissionStrings
)
}
}
}
}
/**
* A singleton that keeps information about whether the app is in the foreground or not.
* This is a simple solution for passing current foreground information from the LocationModule to LocationTaskConsumer.
*/
object AppForegroundedSingleton {
var isForegrounded = false
}
@@ -0,0 +1,864 @@
package expo.modules.location
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.hardware.GeomagneticField
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.location.Geocoder
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.os.Looper
import android.util.Log
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.core.app.ActivityCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.os.bundleOf
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationAvailability
import com.google.android.gms.location.LocationCallback
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.LocationSettingsRequest
import expo.modules.core.interfaces.ActivityEventListener
import expo.modules.core.interfaces.LifecycleEventListener
import expo.modules.core.interfaces.services.UIManager
import expo.modules.interfaces.taskManager.TaskManagerInterface
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.functions.Coroutine
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.location.records.GeocodeResponse
import expo.modules.location.records.GeofencingOptions
import expo.modules.location.records.Heading
import expo.modules.location.records.HeadingEventResponse
import expo.modules.location.records.LocationLastKnownOptions
import expo.modules.location.records.LocationOptions
import expo.modules.location.records.LocationProviderStatus
import expo.modules.location.records.LocationResponse
import expo.modules.location.records.LocationTaskOptions
import expo.modules.location.records.PermissionDetailsLocationAndroid
import expo.modules.location.records.PermissionRequestResponse
import expo.modules.location.records.ReverseGeocodeLocation
import expo.modules.location.records.ReverseGeocodeResponse
import expo.modules.location.taskConsumers.GeofencingTaskConsumer
import expo.modules.location.taskConsumers.LocationTaskConsumer
import java.util.Locale
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.math.abs
class LocationModule : Module(), LifecycleEventListener, SensorEventListener, ActivityEventListener {
private var mGeofield: GeomagneticField? = null
private val mLocationCallbacks = HashMap<Int, LocationCallback>()
private val mLocationRequests = HashMap<Int, LocationRequest>()
private var mPendingLocationRequests = ArrayList<LocationActivityResultListener>()
private lateinit var mContext: Context
private lateinit var mSensorManager: SensorManager
private lateinit var mUIManager: UIManager
private lateinit var mLocationProvider: FusedLocationProviderClient
private var mGravity: FloatArray = FloatArray(9)
private var mGeomagnetic: FloatArray = FloatArray(9)
private var mHeadingId = 0
private var mLastAzimuth = 0f
private var mAccuracy = 0
private var mLastUpdate: Long = 0
private var mGeocoderPaused = false
private val mTaskManager: TaskManagerInterface by lazy {
return@lazy appContext.legacyModule<TaskManagerInterface>()
?: throw TaskManagerNotFoundException()
}
override fun definition() = ModuleDefinition {
Name("ExpoLocation")
OnCreate {
mContext = appContext.reactContext ?: throw Exceptions.ReactContextLost()
mUIManager = appContext.legacyModule<UIManager>() ?: throw MissingUIManagerException()
mLocationProvider = LocationServices.getFusedLocationProviderClient(mContext)
mSensorManager = mContext.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
?: throw SensorManagerUnavailable()
}
Events(HEADING_EVENT_NAME, LOCATION_EVENT_NAME)
// Deprecated
AsyncFunction("requestPermissionsAsync") Coroutine { ->
val permissionsManager = appContext.permissions ?: throw NoPermissionsModuleException()
return@Coroutine if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
LocationHelpers.askForPermissionsWithPermissionsManager(
permissionsManager,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
} else {
LocationHelpers.askForPermissionsWithPermissionsManager(permissionsManager, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
}
}
// Deprecated
AsyncFunction("getPermissionsAsync") Coroutine { ->
val permissionsManager = appContext.permissions ?: throw NoPermissionsModuleException()
return@Coroutine if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
LocationHelpers.getPermissionsWithPermissionsManager(
permissionsManager,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
} else {
getForegroundPermissionsAsync()
}
}
AsyncFunction("requestForegroundPermissionsAsync") Coroutine { ->
val permissionsManager = appContext.permissions ?: throw NoPermissionsModuleException()
LocationHelpers.askForPermissionsWithPermissionsManager(permissionsManager, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
// We aren't using the values returned above, because we need to check if the user has provided fine location permissions
return@Coroutine getForegroundPermissionsAsync()
}
AsyncFunction("requestBackgroundPermissionsAsync") Coroutine { ->
return@Coroutine requestBackgroundPermissionsAsync()
}
AsyncFunction("getForegroundPermissionsAsync") Coroutine { ->
return@Coroutine getForegroundPermissionsAsync()
}
AsyncFunction("getBackgroundPermissionsAsync") Coroutine { ->
return@Coroutine getBackgroundPermissionsAsync()
}
AsyncFunction("getLastKnownPositionAsync") Coroutine { options: LocationLastKnownOptions ->
return@Coroutine getLastKnownPositionAsync(options)
}
AsyncFunction("getCurrentPositionAsync") { options: LocationOptions, promise: Promise ->
return@AsyncFunction getCurrentPositionAsync(options, promise)
}
AsyncFunction<LocationProviderStatus>("getProviderStatusAsync") {
return@AsyncFunction getProviderStatus()
}
AsyncFunction("watchDeviceHeading") { watchId: Int ->
mHeadingId = watchId
return@AsyncFunction startHeadingUpdate()
}
AsyncFunction("watchPositionImplAsync") { watchId: Int, options: LocationOptions, promise: Promise ->
// Check for permissions
if (isMissingForegroundPermissions()) {
promise.reject(LocationUnauthorizedException())
return@AsyncFunction
}
val locationRequest = LocationHelpers.prepareLocationRequest(options)
val showUserSettingsDialog = options.mayShowUserSettingsDialog
if (LocationHelpers.hasNetworkProviderEnabled(mContext) || !showUserSettingsDialog) {
LocationHelpers.requestContinuousUpdates(this@LocationModule, locationRequest, watchId, promise)
} else {
// Pending requests can ask the user to turn on improved accuracy mode in user's settings.
addPendingLocationRequest(
locationRequest,
object : LocationActivityResultListener {
override fun onResult(resultCode: Int) {
if (resultCode == Activity.RESULT_OK) {
LocationHelpers.requestContinuousUpdates(this@LocationModule, locationRequest, watchId, promise)
} else {
promise.reject(LocationSettingsUnsatisfiedException())
}
}
}
)
}
}
AsyncFunction("removeWatchAsync") { watchId: Int ->
if (isMissingForegroundPermissions()) {
throw LocationUnauthorizedException()
}
// Check if we want to stop watching location or compass
if (watchId == mHeadingId) {
destroyHeadingWatch()
} else {
removeLocationUpdatesForRequest(watchId)
}
}
AsyncFunction("geocodeAsync") Coroutine { address: String ->
return@Coroutine geocode(address)
}
AsyncFunction("reverseGeocodeAsync") Coroutine { location: ReverseGeocodeLocation ->
return@Coroutine reverseGeocode(location)
}
AsyncFunction("enableNetworkProviderAsync") Coroutine { ->
if (LocationHelpers.hasNetworkProviderEnabled(mContext)) {
return@Coroutine null
}
val locationRequest = LocationHelpers.prepareLocationRequest(LocationOptions())
return@Coroutine suspendCoroutine<String?> { continuation ->
addPendingLocationRequest(
locationRequest,
object : LocationActivityResultListener {
override fun onResult(resultCode: Int) {
if (resultCode == Activity.RESULT_OK) {
continuation.resume(null)
} else {
continuation.resumeWithException(LocationSettingsUnsatisfiedException())
}
}
}
)
}
}
AsyncFunction<Boolean>("hasServicesEnabledAsync") {
return@AsyncFunction LocationHelpers.isAnyProviderAvailable(mContext)
}
AsyncFunction("startLocationUpdatesAsync") { taskName: String, options: LocationTaskOptions ->
val shouldUseForegroundService = options.foregroundService != null
if (isMissingForegroundPermissions()) {
throw LocationBackgroundUnauthorizedException()
}
// There are two ways of starting this service.
// 1. As a background location service, this requires the background location permission.
// 2. As a user-initiated foreground service with notification, this does NOT require the background location permission.
if (!shouldUseForegroundService && isMissingBackgroundPermissions()) {
throw LocationBackgroundUnauthorizedException()
}
if (!AppForegroundedSingleton.isForegrounded && options.foregroundService != null) {
throw ForegroundServiceStartNotAllowedException()
}
if (!hasForegroundServicePermissions()) {
throw ForegroundServicePermissionsException()
}
mTaskManager.registerTask(taskName, LocationTaskConsumer::class.java, options.toMutableMap())
return@AsyncFunction
}
AsyncFunction("stopLocationUpdatesAsync") { taskName: String ->
mTaskManager.unregisterTask(taskName, LocationTaskConsumer::class.java)
return@AsyncFunction
}
AsyncFunction("hasStartedLocationUpdatesAsync") { taskName: String ->
return@AsyncFunction mTaskManager.taskHasConsumerOfClass(taskName, LocationTaskConsumer::class.java)
}
AsyncFunction("startGeofencingAsync") { taskName: String, options: GeofencingOptions ->
if (isMissingBackgroundPermissions()) {
throw LocationBackgroundUnauthorizedException()
}
mTaskManager.registerTask(taskName, GeofencingTaskConsumer::class.java, options.toMap())
return@AsyncFunction
}
AsyncFunction("hasStartedGeofencingAsync") { taskName: String ->
if (isMissingBackgroundPermissions()) {
throw LocationBackgroundUnauthorizedException()
}
return@AsyncFunction mTaskManager.taskHasConsumerOfClass(taskName, GeofencingTaskConsumer::class.java)
}
AsyncFunction("stopGeofencingAsync") { taskName: String ->
if (isMissingBackgroundPermissions()) {
throw LocationBackgroundUnauthorizedException()
}
mTaskManager.unregisterTask(taskName, GeofencingTaskConsumer::class.java)
return@AsyncFunction
}
OnActivityEntersForeground {
AppForegroundedSingleton.isForegrounded = true
}
OnActivityEntersBackground {
AppForegroundedSingleton.isForegrounded = false
}
}
private suspend fun getForegroundPermissionsAsync(): PermissionRequestResponse {
appContext.permissions?.let {
val locationPermission = LocationHelpers.getPermissionsWithPermissionsManager(it, Manifest.permission.ACCESS_COARSE_LOCATION)
val fineLocationPermission = LocationHelpers.getPermissionsWithPermissionsManager(it, Manifest.permission.ACCESS_FINE_LOCATION)
var accuracy = "none"
if (locationPermission.granted) {
accuracy = "coarse"
}
if (fineLocationPermission.granted) {
accuracy = "fine"
}
locationPermission.android = PermissionDetailsLocationAndroid(
accuracy = accuracy
)
return locationPermission
} ?: throw NoPermissionsModuleException()
}
private fun getProviderStatus(): LocationProviderStatus {
val manager = mContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val isGpsAvailable = manager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkAvailable = manager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
val isLocationServicesEnabled = LocationManagerCompat.isLocationEnabled(manager)
val isPassiveAvailable = manager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER)
return LocationProviderStatus().apply {
backgroundModeEnabled = isLocationServicesEnabled
gpsAvailable = isGpsAvailable
networkAvailable = isNetworkAvailable
locationServicesEnabled = isLocationServicesEnabled
passiveAvailable = isPassiveAvailable
}
}
private suspend fun requestBackgroundPermissionsAsync(): PermissionRequestResponse {
if (!isBackgroundPermissionInManifest()) {
throw NoPermissionInManifestException("ACCESS_BACKGROUND_LOCATION")
}
if (!shouldAskBackgroundPermissions()) {
return getForegroundPermissionsAsync()
}
return appContext.permissions?.let {
val permissionResponseBundle = LocationHelpers.askForPermissionsWithPermissionsManager(it, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
PermissionRequestResponse(permissionResponseBundle)
} ?: throw NoPermissionsModuleException()
}
private suspend fun getBackgroundPermissionsAsync(): PermissionRequestResponse {
if (!isBackgroundPermissionInManifest()) {
throw NoPermissionInManifestException("ACCESS_BACKGROUND_LOCATION")
}
if (!shouldAskBackgroundPermissions()) {
return getForegroundPermissionsAsync()
}
appContext.permissions?.let {
return LocationHelpers.getPermissionsWithPermissionsManager(it, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
} ?: throw NoPermissionsModuleException()
}
/**
* Resolves to the last known position if it is available and matches given requirements or null otherwise.
*/
private suspend fun getLastKnownPositionAsync(options: LocationLastKnownOptions): LocationResponse? {
// Check for permissions
if (isMissingForegroundPermissions()) {
throw LocationUnauthorizedException()
}
val lastKnownLocation = getLastKnownLocation() ?: return null
if (LocationHelpers.isLocationValid(lastKnownLocation, options)) {
return LocationResponse(lastKnownLocation)
}
return null
}
/**
* Requests for the current position. Depending on given accuracy, it may take some time to resolve.
* If you don't need an up-to-date location see `getLastKnownPosition`.
*/
private fun getCurrentPositionAsync(options: LocationOptions, promise: Promise) {
// Read options
val locationRequest = LocationHelpers.prepareLocationRequest(options)
val currentLocationRequest = LocationHelpers.prepareCurrentLocationRequest(options)
val showUserSettingsDialog = options.mayShowUserSettingsDialog
// Check for permissions
if (isMissingForegroundPermissions()) {
promise.reject(LocationUnauthorizedException())
return
}
if (LocationHelpers.hasNetworkProviderEnabled(mContext) || !showUserSettingsDialog) {
LocationHelpers.requestSingleLocation(mLocationProvider, currentLocationRequest, promise)
} else {
addPendingLocationRequest(
locationRequest,
object : LocationActivityResultListener {
override fun onResult(resultCode: Int) {
if (resultCode == Activity.RESULT_OK) {
LocationHelpers.requestSingleLocation(mLocationProvider, currentLocationRequest, promise)
} else {
promise.reject(LocationSettingsUnsatisfiedException())
}
}
}
)
}
}
fun requestLocationUpdates(locationRequest: LocationRequest, requestId: Int?, callbacks: LocationRequestCallbacks) {
val locationProvider: FusedLocationProviderClient = mLocationProvider
val locationCallback: LocationCallback = object : LocationCallback() {
var isLocationAvailable = false
override fun onLocationResult(locationResult: LocationResult) {
val location = locationResult.lastLocation
if (location != null) {
callbacks.onLocationChanged(location)
} else if (!isLocationAvailable) {
callbacks.onLocationError(LocationUnavailableException())
} else {
callbacks.onRequestFailed(LocationUnknownException())
}
}
override fun onLocationAvailability(locationAvailability: LocationAvailability) {
isLocationAvailable = locationAvailability.isLocationAvailable
}
}
if (requestId != null) {
// Save location callback and request so we will be able to pause/resume receiving updates.
mLocationCallbacks[requestId] = locationCallback
mLocationRequests[requestId] = locationRequest
}
try {
locationProvider.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
callbacks.onRequestSuccess()
} catch (e: SecurityException) {
callbacks.onRequestFailed(LocationRequestRejectedException(e))
}
}
private fun addPendingLocationRequest(locationRequest: LocationRequest, listener: LocationActivityResultListener) {
// Add activity result listener to an array of pending requests.
mPendingLocationRequests.add(listener)
// If it's the first pending request, let's ask the user to turn on high accuracy location.
if (mPendingLocationRequests.size == 1) {
resolveUserSettingsForRequest(locationRequest)
}
}
/**
* Triggers system's dialog to ask the user to enable settings required for given location request.
*/
private fun resolveUserSettingsForRequest(locationRequest: LocationRequest) {
val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
val client = LocationServices.getSettingsClient(mContext)
val task = client.checkLocationSettings(builder.build())
task.addOnSuccessListener {
// All location settings requirements are satisfied.
executePendingRequests(Activity.RESULT_OK)
}
task.addOnFailureListener { e: Exception ->
val statusCode = (e as ApiException).statusCode
if (statusCode == CommonStatusCodes.RESOLUTION_REQUIRED) {
// Location settings are not satisfied, but this can be fixed by showing the user a dialog.
// Show the dialog by calling startResolutionForResult(), and check the result in onActivityResult().
try {
val resolvable = e as ResolvableApiException
mUIManager.registerActivityEventListener(this@LocationModule)
resolvable.startResolutionForResult(appContext.throwingActivity, CHECK_SETTINGS_REQUEST_CODE)
} catch (e: Throwable) {
// Ignore the error.
executePendingRequests(Activity.RESULT_CANCELED)
}
} else { // Location settings are not satisfied. However, we have no way to fix the settings so we won't show the dialog.
executePendingRequests(Activity.RESULT_CANCELED)
}
}
}
private fun executePendingRequests(resultCode: Int) {
// Propagate result to pending location requests.
for (listener in mPendingLocationRequests) {
listener.onResult(resultCode)
}
mPendingLocationRequests.clear()
}
private fun startHeadingUpdate() {
val locationManager = mContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
) {
return
}
val lastLocation =
locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
?: locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
if (lastLocation != null) {
mGeofield = GeomagneticField(
lastLocation.latitude.toFloat(),
lastLocation.longitude.toFloat(),
lastLocation.altitude.toFloat(),
System.currentTimeMillis()
)
} else {
val locationRequest = LocationRequest.Builder(
LocationRequest.PRIORITY_HIGH_ACCURACY,
0L
).setMaxUpdates(1)
.build()
val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let {
mGeofield = GeomagneticField(
it.latitude.toFloat(),
it.longitude.toFloat(),
it.altitude.toFloat(),
System.currentTimeMillis()
)
}
}
}
mLocationProvider.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
}
mSensorManager.registerListener(
this,
mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
SensorManager.SENSOR_DELAY_NORMAL
)
mSensorManager.registerListener(
this,
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_NORMAL
)
}
private fun sendUpdate() {
val rotationMatrix = FloatArray(9)
val inclinationMatrix = FloatArray(9)
val success = SensorManager.getRotationMatrix(rotationMatrix, inclinationMatrix, mGravity, mGeomagnetic)
if (success) {
val orientation = FloatArray(3)
SensorManager.getOrientation(rotationMatrix, orientation)
// Make sure Delta is big enough to warrant an update
// Currently: 50ms and ~2 degrees of change (android has a lot of useless updates block up the sending)
if (abs(orientation[0] - mLastAzimuth) > DEGREE_DELTA && System.currentTimeMillis() - mLastUpdate > TIME_DELTA) {
mLastAzimuth = orientation[0]
mLastUpdate = System.currentTimeMillis()
val magneticNorth: Float = calcMagNorth(orientation[0])
val trueNorth: Float = calcTrueNorth(magneticNorth)
// Write data to send back to React
val response = HeadingEventResponse(
watchId = mHeadingId,
heading = Heading(
trueHeading = trueNorth,
magHeading = magneticNorth,
accuracy = mAccuracy
)
)
sendEvent(HEADING_EVENT_NAME, response.toBundle())
}
}
}
internal fun sendLocationResponse(watchId: Int, response: LocationResponse) {
val responseBundle = bundleOf()
responseBundle.putBundle("location", response.toBundle(Bundle::class.java))
responseBundle.putInt("watchId", watchId)
sendEvent(LOCATION_EVENT_NAME, responseBundle)
}
private fun calcMagNorth(azimuth: Float): Float {
val azimuthDeg = Math.toDegrees(azimuth.toDouble()).toFloat()
return (azimuthDeg + 360) % 360
}
private fun calcTrueNorth(magNorth: Float): Float {
// Need to request geo location info to calculate true north
val geofield = mGeofield.takeIf { !isMissingForegroundPermissions() } ?: return -1f
return (magNorth + geofield.declination) % 360
}
private fun stopHeadingWatch() {
mSensorManager.unregisterListener(this)
}
private fun destroyHeadingWatch() {
stopHeadingWatch()
mGravity = FloatArray(9)
mGeomagnetic = FloatArray(9)
mGeofield = null
mHeadingId = 0
mLastAzimuth = 0f
mAccuracy = 0
}
private fun startWatching() {
// if permissions not granted it won't work anyway, but this can be invoked when permission dialog disappears
if (!isMissingForegroundPermissions()) {
mGeocoderPaused = false
}
// Resume paused location updates
resumeLocationUpdates()
}
private fun stopWatching() {
// if permissions not granted it won't work anyway, but this can be invoked when permission dialog appears
if (Geocoder.isPresent() && !isMissingForegroundPermissions()) {
mGeocoderPaused = true
}
for (requestId in mLocationCallbacks.keys) {
pauseLocationUpdatesForRequest(requestId)
}
}
private fun pauseLocationUpdatesForRequest(requestId: Int) {
val locationCallback = mLocationCallbacks[requestId]
if (locationCallback != null) {
mLocationProvider.removeLocationUpdates(locationCallback)
}
}
private fun removeLocationUpdatesForRequest(requestId: Int) {
pauseLocationUpdatesForRequest(requestId)
mLocationCallbacks.remove(requestId)
mLocationRequests.remove(requestId)
}
private fun resumeLocationUpdates() {
for (requestId in mLocationCallbacks.keys) {
val locationCallback = mLocationCallbacks[requestId] ?: return
val locationRequest = mLocationRequests[requestId] ?: return
try {
mLocationProvider.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
} catch (e: SecurityException) {
Log.e(TAG, "Error occurred while resuming location updates: $e")
}
}
}
/**
* Gets the best most recent location found by the provider.
*/
private suspend fun getLastKnownLocation(): Location? {
return suspendCoroutine { continuation ->
try {
mLocationProvider.lastLocation
.addOnSuccessListener { location: Location? -> continuation.resume(location) }
.addOnCanceledListener { continuation.resume(null) }
.addOnFailureListener { continuation.resume(null) }
} catch (e: SecurityException) {
continuation.resume(null)
}
}
}
private suspend fun geocode(address: String): List<GeocodeResponse> {
if (mGeocoderPaused) {
throw GeocodeException("Geocoder is not running")
}
if (isMissingForegroundPermissions()) {
throw LocationUnauthorizedException()
}
if (!Geocoder.isPresent()) {
throw NoGeocodeException()
}
return suspendCoroutine { continuation ->
val locations = Geocoder(mContext, Locale.getDefault()).getFromLocationName(address, 1)
locations?.let { location ->
location.let {
val results = it.mapNotNull { address ->
val newLocation = Location(LocationManager.GPS_PROVIDER)
newLocation.latitude = address.latitude
newLocation.longitude = address.longitude
GeocodeResponse.from(newLocation)
}
continuation.resume(results)
}
} ?: continuation.resume(emptyList())
}
}
private suspend fun reverseGeocode(location: ReverseGeocodeLocation): List<ReverseGeocodeResponse> {
if (mGeocoderPaused) {
throw GeocodeException("Geocoder is not running")
}
if (isMissingForegroundPermissions()) {
throw LocationUnauthorizedException()
}
if (!Geocoder.isPresent()) {
throw NoGeocodeException()
}
val androidLocation = Location("").apply {
latitude = location.latitude
longitude = location.longitude
}
return suspendCoroutine { continuation ->
val locations = Geocoder(mContext, Locale.getDefault()).getFromLocation(androidLocation.latitude, androidLocation.longitude, 1)
locations?.let { addresses ->
val results = addresses.mapNotNull { address ->
address?.let {
ReverseGeocodeResponse(it)
}
}
continuation.resume(results)
} ?: continuation.resume(emptyList())
}
}
//region private methods
/**
* Checks whether all required permissions have been granted by the user.
*/
private fun isMissingForegroundPermissions(): Boolean {
appContext.permissions?.let {
val canAccessFineLocation = it.hasGrantedPermissions(Manifest.permission.ACCESS_FINE_LOCATION)
val canAccessCoarseLocation = it.hasGrantedPermissions(Manifest.permission.ACCESS_COARSE_LOCATION)
return !canAccessFineLocation && !canAccessCoarseLocation
} ?: throw Exceptions.AppContextLost()
}
private fun hasForegroundServicePermissions(): Boolean {
appContext.permissions?.let {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
val canAccessForegroundServiceLocation = it.hasGrantedPermissions(Manifest.permission.FOREGROUND_SERVICE_LOCATION)
val canAccessForegroundService = it.hasGrantedPermissions(Manifest.permission.FOREGROUND_SERVICE)
canAccessForegroundService && canAccessForegroundServiceLocation
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val canAccessForegroundService = it.hasGrantedPermissions(Manifest.permission.FOREGROUND_SERVICE)
canAccessForegroundService
} else {
true
}
} ?: throw Exceptions.AppContextLost()
}
/**
* Checks if the background location permission is granted by the user.
*/
private fun isMissingBackgroundPermissions(): Boolean {
appContext.permissions?.let {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !it.hasGrantedPermissions(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
}
return true
}
/**
* Check if we need to request background location permission separately.
*
* @see `https://medium.com/swlh/request-location-permission-correctly-in-android-11-61afe95a11ad`
*/
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
private fun shouldAskBackgroundPermissions(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
private fun isBackgroundPermissionInManifest(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
appContext.permissions?.let {
return it.isPermissionPresentInManifest(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
}
throw NoPermissionsModuleException()
} else {
true
}
}
/**
* Helper method that lazy-loads the location provider for the context that the module was created.
*/
companion object {
internal val TAG = LocationModule::class.java.simpleName
private const val LOCATION_EVENT_NAME = "Expo.locationChanged"
private const val HEADING_EVENT_NAME = "Expo.headingChanged"
private const val CHECK_SETTINGS_REQUEST_CODE = 42
const val ACCURACY_LOWEST = 1
const val ACCURACY_LOW = 2
const val ACCURACY_BALANCED = 3
const val ACCURACY_HIGH = 4
const val ACCURACY_HIGHEST = 5
const val ACCURACY_BEST_FOR_NAVIGATION = 6
const val GEOFENCING_EVENT_ENTER = 1
const val GEOFENCING_EVENT_EXIT = 2
const val DEGREE_DELTA = 0.0355 // in radians, about 2 degrees
const val TIME_DELTA = 50f // in milliseconds
}
override fun onHostResume() {
startWatching()
startHeadingUpdate()
}
override fun onHostPause() {
stopWatching()
stopHeadingWatch()
}
override fun onHostDestroy() {
stopWatching()
stopHeadingWatch()
}
override fun onSensorChanged(event: SensorEvent?) {
event ?: return
if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
mGravity = event.values
} else if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) {
mGeomagnetic = event.values
}
sendUpdate()
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
mAccuracy = accuracy
}
override fun onActivityResult(activity: Activity?, requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != CHECK_SETTINGS_REQUEST_CODE) {
return
}
executePendingRequests(resultCode)
mUIManager.unregisterActivityEventListener(this)
}
override fun onNewIntent(intent: Intent?) {}
}
@@ -0,0 +1,14 @@
package expo.modules.location
data class LocationParams(
val accuracy: LocationAccuracy,
var distance: Float,
var interval: Long
)
enum class LocationAccuracy {
LOWEST,
LOW,
MEDIUM,
HIGH
}
@@ -0,0 +1,11 @@
package expo.modules.location
import android.location.Location
import expo.modules.kotlin.exception.CodedException
interface LocationRequestCallbacks {
fun onLocationChanged(location: Location) {}
fun onLocationError(cause: CodedException) {}
fun onRequestSuccess() {}
fun onRequestFailed(cause: CodedException) {}
}
@@ -0,0 +1,104 @@
package expo.modules.location.records
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
import expo.modules.location.LocationModule.Companion.ACCURACY_BALANCED
import java.io.Serializable
import expo.modules.kotlin.types.Enumerable
enum class GeofencingRegionState : Enumerable {
UNKNOWN,
INSIDE,
OUTSIDE
}
internal class LocationLastKnownOptions(
@Field var maxAge: Double? = null,
@Field var requiredAccuracy: Double? = null
) : Record, Serializable
internal open class LocationOptions(
@Field var accuracy: Int = ACCURACY_BALANCED,
@Field var distanceInterval: Int? = null,
@Field var mayShowUserSettingsDialog: Boolean = true,
@Field var timeInterval: Long? = null
) : Record, Serializable {
constructor(map: Map<String, Any?>) : this(
accuracy = map["accuracy"] as? Int ?: ACCURACY_BALANCED,
distanceInterval = map["distanceInterval"] as? Int?,
mayShowUserSettingsDialog = map["mayShowUserSettingsDialog"] as? Boolean? ?: true,
timeInterval = map["timeInterval"] as? Long
)
}
internal class ReverseGeocodeLocation(
@Field var latitude: Double,
@Field var longitude: Double,
@Field var accuracy: Float? = null,
@Field var altitude: Double? = null
) : Record, Serializable
internal class LocationTaskOptions(
@Field var deferredUpdatesDistance: Float? = 0f,
@Field var deferredUpdatesInterval: Float? = 0f,
@Field var deferredUpdatesTimeout: Float? = null,
@Field var foregroundService: LocationTaskServiceOptions? = null
) : LocationOptions() {
internal fun toMutableMap(): MutableMap<String, Any?> = mutableMapOf<String, Any?>(
"accuracy" to accuracy,
"distanceInterval" to distanceInterval,
"mayShowUserSettingsDialog" to mayShowUserSettingsDialog,
"timeInterval" to timeInterval,
"deferredUpdatesDistance" to deferredUpdatesDistance,
"deferredUpdatesInterval" to deferredUpdatesInterval,
"deferredUpdatesTimeout" to deferredUpdatesTimeout
).apply {
foregroundService?.let {
this["foregroundService"] = it.toMutableMap()
}
}
}
internal class LocationTaskServiceOptions(
@Field var notificationTitle: String? = null,
@Field var notificationBody: String? = null,
@Field var killServiceOnDestroy: Boolean? = null,
@Field var notificationColor: String? = null
) : Record, Serializable {
internal fun toMutableMap() = mutableMapOf(
"notificationTitle" to notificationTitle,
"notificationBody" to notificationBody,
"killServiceOnDestroy" to killServiceOnDestroy,
"notificationColor" to notificationColor
)
}
internal class GeofencingOptions(
@Field var regions: List<Region>
) : Record, Serializable {
internal fun toMap(): Map<String, Any?> = mapOf(
"regions" to regions.map { it.toMap() }
)
}
internal class Region(
@Field var identifier: String? = null,
@Field var latitude: Double = .0,
@Field var longitude: Double = .0,
@Field var notifyOnEnter: Boolean? = true,
@Field var notifyOnExit: Boolean? = true,
@Field var radius: Double? = .0,
@Field var state: GeofencingRegionState = GeofencingRegionState.UNKNOWN
) : Record, Serializable {
internal fun toMap() = mapOf<String, Any?>(
"identifier" to identifier,
"latitude" to latitude,
"longitude" to longitude,
"notifyOnEnter" to notifyOnEnter,
"notifyOnExit" to notifyOnExit,
"radius" to radius,
"state" to state
)
}
@@ -0,0 +1,218 @@
package expo.modules.location.records
import android.location.Address
import android.location.Location
import android.os.BaseBundle
import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.util.Log
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
import expo.modules.location.ConversionException
import expo.modules.location.LocationModule
import java.io.Serializable
internal class PermissionRequestResponse(
@Field var canAskAgain: Boolean?,
@Field var expires: String?,
@Field var granted: Boolean,
@Field var status: String?,
@Field var android: PermissionDetailsLocationAndroid?
) : Record, Serializable {
constructor(bundle: Bundle) : this(
canAskAgain = bundle.getBoolean("canAskAgain"),
expires = bundle.getString("expires")
?: throw ConversionException(Bundle::class.java, PermissionRequestResponse::class.java, "value under `expires` key is undefined"),
granted = bundle.getBoolean("granted"),
status = bundle.getString("status")
?: throw ConversionException(Bundle::class.java, PermissionRequestResponse::class.java, "value under `status` key is undefined"),
android = bundle.getBundle("android")?.let { PermissionDetailsLocationAndroid(it) }
)
}
internal class PermissionDetailsLocationAndroid(
@Field var accuracy: String
) : Record, Serializable {
constructor(bundle: Bundle) : this(
accuracy = (bundle.getString("accuracy") ?: "none")
)
}
internal class LocationProviderStatus(
@Field var backgroundModeEnabled: Boolean? = null,
@Field var gpsAvailable: Boolean? = false,
@Field var networkAvailable: Boolean? = null,
@Field var locationServicesEnabled: Boolean = false,
@Field var passiveAvailable: Boolean? = null
) : Record, Serializable
internal class Heading(
@Field var trueHeading: Float = -1f,
@Field var magHeading: Float = -1f,
@Field var accuracy: Int = 0
) {
internal fun toBundle(): Bundle {
return Bundle().apply {
putFloat("trueHeading", trueHeading)
putFloat("magHeading", magHeading)
putInt("accuracy", accuracy)
}
}
}
internal class HeadingEventResponse(
@Field var watchId: Int? = null,
@Field var heading: Heading? = null
) : Record, Serializable {
internal fun toBundle(): Bundle {
return Bundle().apply {
watchId?.let { putInt("watchId", it) }
heading?.let { putBundle("heading", it.toBundle()) }
}
}
}
internal class LocationResponse(
@Field var coords: LocationObjectCoords? = null,
@Field var timestamp: Double? = null,
@Field var mocked: Boolean? = null
) : Record, Serializable {
constructor(location: Location) : this(
coords = LocationObjectCoords(location),
timestamp = location.time.toDouble(),
mocked = location.isFromMockProvider
)
internal fun <BundleType : BaseBundle> toBundle(bundleTypeClass: Class<BundleType>): BundleType {
val bundle: BundleType = when (bundleTypeClass) {
PersistableBundle::class.java -> PersistableBundle()
else -> Bundle()
} as? BundleType
?: throw ConversionException(LocationResponse::class.java, bundleTypeClass, "Unsupported bundleTypeClass")
return bundle.apply {
timestamp?.let { putDouble("timestamp", it) }
mocked?.let { putBoolean("mocked", it) }
if (bundle is PersistableBundle) {
(this as PersistableBundle).putPersistableBundle("coords", coords?.toBundle(PersistableBundle::class.java))
} else if (bundle is Bundle) {
(this as Bundle).putBundle("coords", coords?.toBundle(Bundle::class.java))
}
}
}
}
internal class LocationObjectCoords(
@Field var latitude: Double? = null,
@Field var longitude: Double? = null,
@Field var altitude: Double? = null,
@Field var accuracy: Double? = null,
@Field var altitudeAccuracy: Double? = null,
@Field var heading: Double? = null,
@Field var speed: Double? = null
) : Record, Serializable {
constructor(location: Location) : this(
latitude = location.latitude,
longitude = location.longitude,
altitude = location.altitude,
accuracy = location.accuracy.toDouble(),
altitudeAccuracy = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
location.verticalAccuracyMeters.toDouble()
} else {
null
},
heading = location.bearing.toDouble(),
speed = location.speed.toDouble()
)
internal fun <BundleType : BaseBundle> toBundle(bundleTypeClass: Class<BundleType>): BundleType {
val bundle: BundleType = when (bundleTypeClass) {
PersistableBundle::class.java -> PersistableBundle()
else -> Bundle()
} as? BundleType
?: throw ConversionException(LocationObjectCoords::class.java, bundleTypeClass, "Requested an unsupported bundle type")
bundle.apply {
latitude?.let { putDouble("latitude", it) }
longitude?.let { putDouble("longitude", it) }
altitude?.let { putDouble("altitude", it) }
accuracy?.let { putDouble("accuracy", it) }
altitudeAccuracy?.let { putDouble("altitudeAccuracy", it) }
heading?.let { putDouble("heading", it) }
speed?.let { putDouble("speed", it) }
}
return bundle
}
}
internal class GeocodeResponse(
@Field var latitude: Double,
@Field var longitude: Double,
@Field var accuracy: Float? = null,
@Field var altitude: Double? = null
) : Record, Serializable {
companion object {
fun from(location: Location): GeocodeResponse? {
return try {
GeocodeResponse(
latitude = location.latitude,
longitude = location.longitude,
accuracy = location.accuracy,
altitude = location.altitude
)
} catch (e: Exception) {
if (e is IllegalAccessException || e is InstantiationException) {
Log.e(LocationModule.TAG, "Unexpected exception was thrown when converting location to coords bundle: ", e)
}
null
}
}
}
}
internal class ReverseGeocodeResponse(
@Field var city: String?,
@Field var district: String?,
@Field var streetNumber: String?,
@Field var street: String?,
@Field var region: String?,
@Field var subregion: String?,
@Field var country: String?,
@Field var postalCode: String?,
@Field var name: String?,
@Field var isoCountryCode: String,
@Field var timezone: String?,
@Field var formattedAddress: String?
) : Record, Serializable {
constructor(address: Address) : this(
city = address.locality,
district = address.subLocality,
streetNumber = address.subThoroughfare,
street = address.thoroughfare,
region = address.adminArea,
subregion = address.subAdminArea,
country = address.countryName,
postalCode = address.postalCode,
name = address.featureName,
isoCountryCode = address.countryCode,
timezone = null,
formattedAddress = constructFormattedAddress(address)
)
companion object {
fun constructFormattedAddress(address: Address): String? {
if (address.maxAddressLineIndex == -1) {
return null
}
val sb = StringBuilder()
for (i in 0..address.maxAddressLineIndex) {
sb.append(address.getAddressLine(i))
if (i < address.maxAddressLineIndex) {
sb.append(", ")
}
}
return sb.toString()
}
}
}
@@ -0,0 +1,120 @@
package expo.modules.location.services
import android.annotation.TargetApi
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
class LocationTaskService : Service() {
private var mChannelId: String? = null
private var mKillService = false
private lateinit var mParentContext: Context
private val mServiceId = sServiceId++
private val mBinder: IBinder = ServiceBinder()
inner class ServiceBinder : Binder() {
val service: LocationTaskService
get() = this@LocationTaskService
}
override fun onBind(intent: Intent): IBinder {
return mBinder
}
@TargetApi(26)
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val extras = intent.extras
if (extras != null) {
mChannelId = extras.getString("appId") + ":" + extras.getString("taskName")
mKillService = extras.getBoolean("killService", false)
}
return START_REDELIVER_INTENT
}
fun setParentContext(context: Context) {
// Background location logic is still outside LocationTaskService,
// so we have to save parent context in order to make sure it won't be destroyed by the OS.
mParentContext = context
}
fun stop() {
stopForeground(true)
stopSelf()
}
override fun onTaskRemoved(rootIntent: Intent) {
if (mKillService) {
super.onTaskRemoved(rootIntent)
stop()
}
}
fun startForeground(serviceOptions: Bundle) {
val notification = buildServiceNotification(serviceOptions)
startForeground(mServiceId, notification)
}
//region private
@TargetApi(26)
private fun buildServiceNotification(serviceOptions: Bundle): Notification {
prepareChannel(mChannelId)
val builder = Notification.Builder(this, mChannelId)
val title = serviceOptions.getString("notificationTitle")
val body = serviceOptions.getString("notificationBody")
val color = colorStringToInteger(serviceOptions.getString("notificationColor"))
title?.let { builder.setContentTitle(title) }
body?.let { builder.setContentText(body) }
color?.let {
builder.setColorized(true).setColor(color)
} ?: run {
builder.setColorized(false)
}
mParentContext.packageManager.getLaunchIntentForPackage(mParentContext.packageName)?.let {
it.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
// We're defaulting to the behaviour prior API 31 (mutable) even though Android recommends immutability
val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
val contentIntent = PendingIntent.getActivity(this, 0, it, PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag)
builder.setContentIntent(contentIntent)
}
return builder.setCategory(Notification.CATEGORY_SERVICE)
.setSmallIcon(applicationInfo.icon)
.build()
}
@TargetApi(26)
private fun prepareChannel(id: String?) {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager
?: return
val appName = applicationInfo.loadLabel(packageManager).toString()
var channel = notificationManager.getNotificationChannel(id)
if (channel == null) {
channel = NotificationChannel(id, appName, NotificationManager.IMPORTANCE_LOW)
channel.description = "Background location notification channel"
notificationManager.createNotificationChannel(channel)
}
}
private fun colorStringToInteger(color: String?): Int? {
return try {
Color.parseColor(color)
} catch (e: Exception) {
null
}
} //endregion
companion object {
private var sServiceId = 481756
}
}
@@ -0,0 +1,245 @@
package expo.modules.location.taskConsumers
import android.app.PendingIntent
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.PersistableBundle
import android.util.Log
import com.google.android.gms.location.Geofence
import com.google.android.gms.location.GeofenceStatusCodes
import com.google.android.gms.location.GeofencingClient
import com.google.android.gms.location.GeofencingEvent
import com.google.android.gms.location.GeofencingRequest
import com.google.android.gms.location.LocationServices
import expo.modules.interfaces.taskManager.TaskConsumer
import expo.modules.interfaces.taskManager.TaskConsumerInterface
import expo.modules.interfaces.taskManager.TaskInterface
import expo.modules.interfaces.taskManager.TaskManagerUtilsInterface
import expo.modules.location.GeofencingException
import expo.modules.location.LocationHelpers
import expo.modules.location.records.GeofencingRegionState
import expo.modules.location.LocationModule
import java.util.UUID
class GeofencingTaskConsumer(context: Context, taskManagerUtils: TaskManagerUtilsInterface?) : TaskConsumer(context, taskManagerUtils), TaskConsumerInterface {
private var mTask: TaskInterface? = null
private var mPendingIntent: PendingIntent? = null
private var mGeofencingClient: GeofencingClient? = null
private var mGeofencingRequest: GeofencingRequest? = null
private var mGeofencingList: MutableList<Geofence> = ArrayList()
private var mRegions: MutableMap<String, PersistableBundle> = HashMap()
//region TaskConsumerInterface
override fun taskType(): String {
return "geofencing"
}
override fun didRegister(task: TaskInterface) {
mTask = task
startGeofencing()
}
override fun didUnregister() {
stopGeofencing()
mTask = null
mPendingIntent = null
mGeofencingClient = null
mGeofencingRequest = null
mGeofencingList.clear()
}
override fun setOptions(options: Map<String, Any>) {
super.setOptions(options)
stopGeofencing()
startGeofencing()
}
override fun didReceiveBroadcast(intent: Intent) {
val event = GeofencingEvent.fromIntent(intent) ?: run {
Log.w(TAG, "Received a null geofencing event. Ignoring")
return
}
if (event.hasError()) {
val errorMessage = getErrorString(event.errorCode)
val error = Error(errorMessage)
mTask?.execute(null, error)
return
}
// Get region state and event type from given transition type.
val geofenceTransition = event.geofenceTransition
val regionState = regionStateForTransitionType(geofenceTransition)
val eventType = eventTypeFromTransitionType(geofenceTransition)
val triggeringGeofences = event.triggeringGeofences ?: return
for (geofence in triggeringGeofences) {
mRegions[geofence.requestId]?.let {
val data = PersistableBundle()
// Update region state in region bundle.
it.putInt("state", regionState.ordinal)
data.putInt("eventType", eventType)
data.putPersistableBundle("region", it)
val context = context.applicationContext
taskManagerUtils.scheduleJob(context, mTask, listOf(data))
}
}
}
override fun didExecuteJob(jobService: JobService, params: JobParameters): Boolean {
val task = mTask ?: return false
val data = taskManagerUtils.extractDataFromJobParams(params)
for (item in data) {
val bundle = Bundle()
val region = Bundle()
region.putAll(item.getPersistableBundle("region"))
bundle.putInt("eventType", item.getInt("eventType"))
bundle.putBundle("region", region)
task.execute(bundle, null) { jobService.jobFinished(params, false) }
}
// Returning `true` indicates that the job is still running, but in async mode.
// In that case we're obligated to call `jobService.jobFinished` as soon as the async block finishes.
return true
}
//endregion
//region helpers
private fun startGeofencing() {
val context = context ?: run {
Log.w(TAG, "The context has been abandoned")
return
}
if (!LocationHelpers.isAnyProviderAvailable(context)) {
Log.w(TAG, "There is no location provider available")
return
}
mRegions = HashMap()
mGeofencingList = ArrayList()
// Create geofences from task options.
val options = mTask?.options
?: throw GeofencingException("Task is null, can't start geofencing")
val regions: List<HashMap<String, Any>> = (options["regions"] as ArrayList<*>).filterIsInstance<HashMap<String, Any>>()
for (region in regions) {
val geofence = geofenceFromRegion(region)
val regionIdentifier = geofence.requestId
// Make a bundle for the region to remember its attributes. Only request ID is public in Geofence object.
mRegions[regionIdentifier] = bundleFromRegion(regionIdentifier, region)
// Add geofence to the list of observed regions.
mGeofencingList.add(geofence)
}
// Prepare pending intent, geofencing request and client.
mPendingIntent = preparePendingIntent()
mGeofencingRequest = prepareGeofencingRequest(mGeofencingList)
mGeofencingClient = LocationServices.getGeofencingClient(getContext())
try {
mPendingIntent?.let { pendingIntent ->
mGeofencingRequest?.let { geofencingRequest ->
mGeofencingClient?.addGeofences(geofencingRequest, pendingIntent)
}
}
} catch (e: SecurityException) {
Log.w(TAG, "Geofencing request has been rejected.", e)
}
}
private fun stopGeofencing() {
mPendingIntent?.let {
mGeofencingClient?.removeGeofences(it)
it.cancel()
}
}
private fun prepareGeofencingRequest(geofences: List<Geofence>): GeofencingRequest {
return GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER or GeofencingRequest.INITIAL_TRIGGER_EXIT)
.addGeofences(geofences)
.build()
}
private fun preparePendingIntent(): PendingIntent {
return taskManagerUtils.createTaskIntent(context, mTask)
}
private fun getParamAsDouble(param: Any?, errorMessage: String): Double {
return when (param) {
is Double -> param
is Float -> param.toDouble()
is Int -> param.toDouble()
is Long -> param.toDouble()
is String -> param.toDoubleOrNull()
else -> null
} ?: throw GeofencingException(errorMessage)
}
private fun geofenceFromRegion(region: Map<String, Any>): Geofence {
val identifier = region["identifier"] as? String ?: UUID.randomUUID().toString()
val radius = getParamAsDouble(region["radius"], "Region: radius: `${region["radius"]}` can't be cast to Double")
val longitude = getParamAsDouble(region["longitude"], "Region: longitude: `${region["longitude"]}` can't be cast to Double")
val latitude = getParamAsDouble(region["latitude"], "Region: latitude `${region["latitude"]}` can't be cast to Double")
val notifyOnEnter = region["notifyOnEnter"] as? Boolean ?: true
val notifyOnExit = region["notifyOnExit"] as? Boolean ?: true
val transitionTypes = (if (notifyOnEnter) Geofence.GEOFENCE_TRANSITION_ENTER else 0) or if (notifyOnExit) Geofence.GEOFENCE_TRANSITION_EXIT else 0
return Geofence.Builder()
.setRequestId(identifier)
.setCircularRegion(latitude, longitude, radius.toFloat())
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(transitionTypes)
.build()
}
private fun bundleFromRegion(identifier: String, region: Map<String, Any>): PersistableBundle {
return PersistableBundle().apply {
val radius = getParamAsDouble(region["radius"], "Region: radius: `${region["radius"]}` can't be cast to Double")
val longitude = getParamAsDouble(region["longitude"], "Region: longitude: `${region["longitude"]}` can't be cast to Double")
val latitude = getParamAsDouble(region["latitude"], "Region: latitude: `${region["latitude"]}` can't be cast to Double")
putString("identifier", identifier)
putDouble("radius", radius)
putDouble("latitude", latitude)
putDouble("longitude", longitude)
putInt("state", GeofencingRegionState.UNKNOWN.ordinal)
}
}
private fun regionStateForTransitionType(transitionType: Int): GeofencingRegionState {
return when (transitionType) {
Geofence.GEOFENCE_TRANSITION_ENTER, Geofence.GEOFENCE_TRANSITION_DWELL -> GeofencingRegionState.INSIDE
Geofence.GEOFENCE_TRANSITION_EXIT -> GeofencingRegionState.OUTSIDE
else -> GeofencingRegionState.UNKNOWN
}
}
private fun eventTypeFromTransitionType(transitionType: Int): Int {
return when (transitionType) {
Geofence.GEOFENCE_TRANSITION_ENTER -> LocationModule.GEOFENCING_EVENT_ENTER
Geofence.GEOFENCE_TRANSITION_EXIT -> LocationModule.GEOFENCING_EVENT_EXIT
else -> 0
}
}
companion object {
private const val TAG = "GeofencingTaskConsumer"
private fun getErrorString(errorCode: Int): String {
return when (errorCode) {
GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE -> "Geofencing not available."
GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES -> "Too many geofences."
GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS -> "Too many pending intents."
else -> "Unknown geofencing error."
}
} //endregion
}
}
@@ -0,0 +1,322 @@
package expo.modules.location.taskConsumers
import android.app.PendingIntent
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.location.Location
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.os.PersistableBundle
import android.util.Log
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import expo.modules.core.MapHelper
import expo.modules.core.arguments.MapArguments
import expo.modules.core.arguments.ReadableArguments
import expo.modules.core.interfaces.Arguments
import expo.modules.core.interfaces.LifecycleEventListener
import expo.modules.interfaces.taskManager.TaskConsumer
import expo.modules.interfaces.taskManager.TaskConsumerInterface
import expo.modules.interfaces.taskManager.TaskExecutionCallback
import expo.modules.interfaces.taskManager.TaskInterface
import expo.modules.interfaces.taskManager.TaskManagerUtilsInterface
import expo.modules.location.AppForegroundedSingleton
import expo.modules.location.LocationHelpers
import expo.modules.location.records.LocationOptions
import expo.modules.location.records.LocationResponse
import expo.modules.location.services.LocationTaskService
import expo.modules.location.services.LocationTaskService.ServiceBinder
import kotlin.math.abs
class LocationTaskConsumer(context: Context, taskManagerUtils: TaskManagerUtilsInterface?) : TaskConsumer(context, taskManagerUtils), TaskConsumerInterface, LifecycleEventListener {
private var mTask: TaskInterface? = null
private var mPendingIntent: PendingIntent? = null
private var mService: LocationTaskService? = null
private var mLocationRequest: LocationRequest? = null
private var mLastReportedLocation: Location? = null
private var mDeferredDistance = 0.0
private val mDeferredLocations: MutableList<Location> = ArrayList()
private var mIsHostPaused = true
private val mLocationClient: FusedLocationProviderClient by lazy {
LocationServices.getFusedLocationProviderClient(context)
}
//region TaskConsumerInterface
override fun taskType(): String {
return "location"
}
override fun didRegister(task: TaskInterface) {
mTask = task
startLocationUpdates()
maybeStartForegroundService()
}
override fun didUnregister() {
stopLocationUpdates()
stopForegroundService()
mTask = null
mPendingIntent = null
mLocationRequest = null
}
override fun setOptions(options: Map<String, Any>) {
super.setOptions(options)
// Restart location updates
stopLocationUpdates()
startLocationUpdates()
// Restart foreground service if its option has changed.
maybeStartForegroundService()
}
override fun didReceiveBroadcast(intent: Intent) {
mTask ?: return
val result = LocationResult.extractResult(intent)
if (result != null) {
val locations = result.locations
deferLocations(locations)
maybeReportDeferredLocations()
} else {
try {
mLocationClient.lastLocation.addOnCompleteListener { task ->
task.result?.let {
deferLocations(listOf(it))
maybeReportDeferredLocations()
}
}
} catch (e: SecurityException) {
Log.e(TAG, "Cannot get last location: " + e.message)
}
}
}
override fun didExecuteJob(jobService: JobService, params: JobParameters): Boolean {
val data = taskManagerUtils.extractDataFromJobParams(params)
val locationBundles = ArrayList<Bundle>()
for (persistableLocationBundle in data) {
val locationBundle = Bundle()
val coordsBundle = Bundle()
if (persistableLocationBundle != null) {
coordsBundle.putAll(persistableLocationBundle.getPersistableBundle("coords"))
locationBundle.putAll(persistableLocationBundle)
locationBundle.putBundle("coords", coordsBundle)
locationBundles.add(locationBundle)
}
}
executeTaskWithLocationBundles(locationBundles) { jobService.jobFinished(params, false) }
// Returning `true` indicates that the job is still running, but in async mode.
// In that case we're obligated to call `jobService.jobFinished` as soon as the async block finishes.
return true
}
//region private
private fun startLocationUpdates() {
val context = context ?: run {
Log.w(TAG, "The context has been abandoned")
return
}
if (!LocationHelpers.isAnyProviderAvailable(context)) {
Log.w(TAG, "There is no location provider available")
return
}
val task = mTask ?: run {
Log.w(TAG, "Could not find a location task for the location update")
return
}
mLocationRequest = LocationHelpers.prepareLocationRequest(LocationOptions(task.options))
mPendingIntent = preparePendingIntent()
val locationRequest = mLocationRequest ?: run {
Log.w(TAG, "Could not find a location request for the location update")
return
}
val intent = mPendingIntent ?: run {
Log.w(TAG, "Could not find intent for the location update")
return
}
try {
mLocationClient.requestLocationUpdates(locationRequest, intent)
} catch (e: SecurityException) {
Log.w(TAG, "Location request has been rejected.", e)
}
}
private fun stopLocationUpdates() {
mPendingIntent?.let {
mLocationClient.removeLocationUpdates(it)
it.cancel()
}
}
private fun maybeStartForegroundService() {
// Foreground service is available as of Android Oreo.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
if (!AppForegroundedSingleton.isForegrounded) {
Log.w(TAG, "Foreground location task cannot be started while the app is in the background!")
return
}
val task = mTask ?: run {
Log.w(TAG, "Location task is null")
return
}
val options: ReadableArguments = MapArguments(task.options)
val useForegroundService = shouldUseForegroundService(task.options)
// Service is already running, but the task has been registered again without `foregroundService` option.
if (mService != null && !useForegroundService) {
stopForegroundService()
return
}
// Service is not running and the user don't want to start foreground service.
if (!useForegroundService) {
return
}
// Foreground service is requested but not running.
if (mService == null) {
val serviceIntent = Intent(context, LocationTaskService::class.java)
val extras = Bundle()
val serviceOptions = options.getArguments(FOREGROUND_SERVICE_KEY).toBundle()
// extras param name is appId for legacy reasons
extras.putString("appId", task.appScopeKey)
extras.putString("taskName", task.name)
extras.putBoolean("killService", serviceOptions.getBoolean("killServiceOnDestroy", false))
serviceIntent.putExtras(extras)
context.startForegroundService(serviceIntent)
context.bindService(
serviceIntent,
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
mService = (service as? ServiceBinder)?.service
mService?.let {
it.setParentContext(context)
it.startForeground(serviceOptions)
}
}
override fun onServiceDisconnected(name: ComponentName) {
mService?.stop()
mService = null
}
},
Context.BIND_AUTO_CREATE
)
} else {
// Restart the service with new service options.
mService?.startForeground(options.getArguments(FOREGROUND_SERVICE_KEY).toBundle())
}
}
private fun stopForegroundService() {
mService?.stop()
}
private fun deferLocations(locations: List<Location>) {
val size = mDeferredLocations.size
var lastLocation = if (size > 0) mDeferredLocations[size - 1] else mLastReportedLocation
for (location in locations) {
if (lastLocation != null) {
mDeferredDistance += abs(location.distanceTo(lastLocation)).toDouble()
}
lastLocation = location
}
mDeferredLocations.addAll(locations)
}
private fun maybeReportDeferredLocations() {
if (!shouldReportDeferredLocations()) {
return
}
val context = context.applicationContext
val data: MutableList<PersistableBundle> = ArrayList()
for (location in mDeferredLocations) {
val timestamp = location.time
// Some devices may broadcast the same location multiple times (mostly twice) so we're filtering out these locations,
// so only one location at the specific timestamp can schedule a job.
if (timestamp > sLastTimestamp) {
val bundle = LocationResponse(location).toBundle(PersistableBundle::class.java)
data.add(bundle)
sLastTimestamp = timestamp
}
}
if (data.size > 0) {
// Save last reported location, reset the distance and clear a list of locations.
mLastReportedLocation = mDeferredLocations[mDeferredLocations.size - 1]
mDeferredDistance = 0.0
mDeferredLocations.clear()
// Schedule new job.
taskManagerUtils.scheduleJob(context, mTask, data)
}
}
private fun shouldReportDeferredLocations(): Boolean {
val task = mTask ?: return false
if (mDeferredLocations.size == 0) {
return false
}
if (!mIsHostPaused) {
// Don't defer location updates when the activity is in foreground state.
return true
}
val oldestLocation = mLastReportedLocation ?: mDeferredLocations[0]
val newestLocation = mDeferredLocations[mDeferredLocations.size - 1]
val options: Arguments = MapHelper(task.options)
val distance = options.getDouble("deferredUpdatesDistance")
val interval = options.getLong("deferredUpdatesInterval")
return newestLocation.time - oldestLocation.time >= interval && mDeferredDistance >= distance
}
private fun preparePendingIntent(): PendingIntent {
return taskManagerUtils.createTaskIntent(context, mTask)
}
private fun executeTaskWithLocationBundles(locationBundles: ArrayList<Bundle>, callback: TaskExecutionCallback) {
if (locationBundles.size > 0 && mTask != null) {
val data = Bundle()
data.putParcelableArrayList("locations", locationBundles)
mTask?.execute(data, null, callback)
} else {
callback.onFinished(null)
}
}
override fun onHostResume() {
mIsHostPaused = false
maybeReportDeferredLocations()
}
override fun onHostPause() {
mIsHostPaused = true
}
override fun onHostDestroy() {
mIsHostPaused = true
} //endregion
companion object {
private const val TAG = "LocationTaskConsumer"
private const val FOREGROUND_SERVICE_KEY = "foregroundService"
private var sLastTimestamp: Long = 0
fun shouldUseForegroundService(options: Map<String?, Any?>): Boolean {
return options.containsKey(FOREGROUND_SERVICE_KEY)
}
}
}
+1
View File
@@ -0,0 +1 @@
module.exports = require('./plugin/build/withLocation');
+3
View File
@@ -0,0 +1,3 @@
declare const _default: any;
export default _default;
//# sourceMappingURL=ExpoLocation.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"ExpoLocation.d.ts","sourceRoot":"","sources":["../src/ExpoLocation.ts"],"names":[],"mappings":";AACA,wBAAmD"}
+3
View File
@@ -0,0 +1,3 @@
import { requireNativeModule } from 'expo-modules-core';
export default requireNativeModule('ExpoLocation');
//# sourceMappingURL=ExpoLocation.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"ExpoLocation.js","sourceRoot":"","sources":["../src/ExpoLocation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,eAAe,mBAAmB,CAAC,cAAc,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from 'expo-modules-core';\nexport default requireNativeModule('ExpoLocation');\n"]}
+21
View File
@@ -0,0 +1,21 @@
import { PermissionResponse } from 'expo-modules-core';
import { LocationLastKnownOptions, LocationObject, LocationOptions } from './Location.types';
declare const _default: {
getProviderStatusAsync(): Promise<{
locationServicesEnabled: boolean;
}>;
getLastKnownPositionAsync(options?: LocationLastKnownOptions): Promise<LocationObject | null>;
getCurrentPositionAsync(options: LocationOptions): Promise<LocationObject>;
removeWatchAsync(watchId: number): Promise<void>;
watchDeviceHeading(_headingId: number): Promise<void>;
hasServicesEnabledAsync(): Promise<boolean>;
geocodeAsync(): Promise<any[]>;
reverseGeocodeAsync(): Promise<any[]>;
watchPositionImplAsync(watchId: number, options: PositionOptions): Promise<number>;
requestForegroundPermissionsAsync(): Promise<PermissionResponse>;
requestBackgroundPermissionsAsync(): Promise<PermissionResponse>;
getForegroundPermissionsAsync(): Promise<PermissionResponse>;
getBackgroundPermissionsAsync(): Promise<PermissionResponse>;
};
export default _default;
//# sourceMappingURL=ExpoLocation.web.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"ExpoLocation.web.d.ts","sourceRoot":"","sources":["../src/ExpoLocation.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAyC,MAAM,mBAAmB,CAAC;AAE9F,OAAO,EAEL,wBAAwB,EACxB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;;8BAuHQ,OAAO,CAAC;QAAE,uBAAuB,EAAE,OAAO,CAAA;KAAE,CAAC;wCAMlE,wBAAwB,GAChC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;qCAMM,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;8BAahD,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;mCAGjB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;+BAG1B,OAAO,CAAC,OAAO,CAAC;oBAG3B,OAAO,CAAC,GAAG,EAAE,CAAC;2BAGP,OAAO,CAAC,GAAG,EAAE,CAAC;oCAGL,MAAM,WAAW,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;yCAiB7C,OAAO,CAAC,kBAAkB,CAAC;yCAG3B,OAAO,CAAC,kBAAkB,CAAC;qCAG/B,OAAO,CAAC,kBAAkB,CAAC;qCAG3B,OAAO,CAAC,kBAAkB,CAAC;;AApEpE,wBAuEE"}
+167
View File
@@ -0,0 +1,167 @@
import { PermissionStatus, UnavailabilityError } from 'expo-modules-core';
import { LocationAccuracy, } from './Location.types';
import { LocationEventEmitter } from './LocationEventEmitter';
class GeocoderError extends Error {
code;
constructor() {
super('Geocoder service is not available for this device.');
this.code = 'E_NO_GEOCODER';
}
}
/**
* Converts `GeolocationPosition` to JavaScript object.
*/
function geolocationPositionToJSON(position) {
const { coords, timestamp } = position;
return {
coords: {
latitude: coords.latitude,
longitude: coords.longitude,
altitude: coords.altitude,
accuracy: coords.accuracy,
altitudeAccuracy: coords.altitudeAccuracy,
heading: coords.heading,
speed: coords.speed,
},
timestamp,
};
}
/**
* Checks whether given location didn't exceed given `maxAge` and fits in the required accuracy.
*/
function isLocationValid(location, options) {
const maxAge = typeof options.maxAge === 'number' ? options.maxAge : Infinity;
const requiredAccuracy = typeof options.requiredAccuracy === 'number' ? options.requiredAccuracy : Infinity;
const locationAccuracy = location.coords.accuracy ?? Infinity;
return Date.now() - location.timestamp <= maxAge && locationAccuracy <= requiredAccuracy;
}
/**
* Gets the permission details. The implementation is not very good as it's not
* possible to query for permission on all browsers, apparently only the
* latest versions will support this.
*/
async function getPermissionsAsync(shouldAsk = false) {
if (!navigator?.permissions?.query) {
throw new UnavailabilityError('expo-location', 'navigator.permissions API is not available');
}
const permission = await navigator.permissions.query({ name: 'geolocation' });
if (permission.state === 'granted') {
return {
status: PermissionStatus.GRANTED,
granted: true,
canAskAgain: true,
expires: 0,
};
}
if (permission.state === 'denied') {
return {
status: PermissionStatus.DENIED,
granted: false,
canAskAgain: true,
expires: 0,
};
}
if (shouldAsk) {
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(() => {
resolve({
status: PermissionStatus.GRANTED,
granted: true,
canAskAgain: true,
expires: 0,
});
}, (positionError) => {
if (positionError.code === positionError.PERMISSION_DENIED) {
resolve({
status: PermissionStatus.DENIED,
granted: false,
canAskAgain: true,
expires: 0,
});
return;
}
resolve({
status: PermissionStatus.GRANTED,
granted: false,
canAskAgain: true,
expires: 0,
});
});
});
}
// The permission state is 'prompt' when the permission has not been requested
// yet, tested on Chrome.
return {
status: PermissionStatus.UNDETERMINED,
granted: false,
canAskAgain: true,
expires: 0,
};
}
let lastKnownPosition = null;
export default {
async getProviderStatusAsync() {
return {
locationServicesEnabled: 'geolocation' in navigator,
};
},
async getLastKnownPositionAsync(options = {}) {
if (lastKnownPosition && isLocationValid(lastKnownPosition, options)) {
return lastKnownPosition;
}
return null;
},
async getCurrentPositionAsync(options) {
return new Promise((resolve, reject) => {
const resolver = (position) => {
lastKnownPosition = geolocationPositionToJSON(position);
resolve(lastKnownPosition);
};
navigator.geolocation.getCurrentPosition(resolver, reject, {
maximumAge: Infinity,
enableHighAccuracy: (options.accuracy ?? 0) > LocationAccuracy.Balanced,
...options,
});
});
},
async removeWatchAsync(watchId) {
navigator.geolocation.clearWatch(watchId);
},
async watchDeviceHeading(_headingId) {
console.warn('Location.watchDeviceHeading: is not supported on web');
},
async hasServicesEnabledAsync() {
return 'geolocation' in navigator;
},
async geocodeAsync() {
throw new GeocoderError();
},
async reverseGeocodeAsync() {
throw new GeocoderError();
},
async watchPositionImplAsync(watchId, options) {
return new Promise((resolve) => {
watchId = navigator.geolocation.watchPosition((position) => {
lastKnownPosition = geolocationPositionToJSON(position);
LocationEventEmitter.emit('Expo.locationChanged', {
watchId,
location: lastKnownPosition,
});
}, undefined, options);
resolve(watchId);
});
},
async requestForegroundPermissionsAsync() {
return getPermissionsAsync(true);
},
async requestBackgroundPermissionsAsync() {
return getPermissionsAsync(true);
},
async getForegroundPermissionsAsync() {
return getPermissionsAsync();
},
async getBackgroundPermissionsAsync() {
return getPermissionsAsync();
},
};
//# sourceMappingURL=ExpoLocation.web.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
/**
* Polyfills `navigator.geolocation` for interop with the core React Native and Web API approach to geolocation.
*/
export declare function installWebGeolocationPolyfill(): void;
//# sourceMappingURL=GeolocationPolyfill.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"GeolocationPolyfill.d.ts","sourceRoot":"","sources":["../src/GeolocationPolyfill.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,wBAAgB,6BAA6B,IAAI,IAAI,CAiBpD"}
+59
View File
@@ -0,0 +1,59 @@
import { Platform } from 'expo-modules-core';
import ExpoLocation from './ExpoLocation';
import { LocationAccuracy } from './Location.types';
import { LocationSubscriber } from './LocationSubscribers';
// @needsAudit
/**
* Polyfills `navigator.geolocation` for interop with the core React Native and Web API approach to geolocation.
*/
export function installWebGeolocationPolyfill() {
if (Platform.OS !== 'web') {
// Make sure `window.navigator` is defined in the global scope.
if (!('window' in global)) {
global.window = global;
}
if (!('navigator' in global.window)) {
global.window.navigator = {};
}
// @ts-ignore
window.navigator.geolocation = {
getCurrentPosition,
watchPosition,
clearWatch,
};
}
}
function convertGeolocationOptions(options) {
return {
accuracy: options.enableHighAccuracy ? LocationAccuracy.High : LocationAccuracy.Balanced,
};
}
function getCurrentPosition(success, error = () => { }, options = {}) {
_getCurrentPositionAsyncWrapper(success, error, options);
}
// This function exists to let us continue to return undefined from getCurrentPosition, while still
// using async/await for the internal implementation of it
async function _getCurrentPositionAsyncWrapper(success, error, options) {
try {
await ExpoLocation.requestPermissionsAsync();
const result = await ExpoLocation.getCurrentPositionAsync(convertGeolocationOptions(options));
success(result);
}
catch (e) {
error(e);
}
}
// Polyfill: navigator.geolocation.watchPosition
function watchPosition(success, error, options) {
const watchId = LocationSubscriber.registerCallback(success);
ExpoLocation.watchPositionImplAsync(watchId, options).catch((err) => {
LocationSubscriber.unregisterCallback(watchId);
error({ watchId, message: err.message, code: err.code });
});
return watchId;
}
// Polyfill: navigator.geolocation.clearWatch
function clearWatch(watchId) {
LocationSubscriber.unregisterCallback(watchId);
}
//# sourceMappingURL=GeolocationPolyfill.js.map
@@ -0,0 +1 @@
{"version":3,"file":"GeolocationPolyfill.js","sourceRoot":"","sources":["../src/GeolocationPolyfill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAkB,gBAAgB,EAAmB,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAW3D,cAAc;AACd;;GAEG;AACH,MAAM,UAAU,6BAA6B;IAC3C,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,+DAA+D;QAC/D,IAAI,CAAC,CAAC,QAAQ,IAAI,MAAM,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;QACzB,CAAC;QACD,IAAI,CAAC,CAAC,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YACpC,MAAM,CAAC,MAAM,CAAC,SAAS,GAAG,EAAE,CAAC;QAC/B,CAAC;QAED,aAAa;QACb,MAAM,CAAC,SAAS,CAAC,WAAW,GAAG;YAC7B,kBAAkB;YAClB,aAAa;YACb,UAAU;SACX,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,yBAAyB,CAAC,OAA2B;IAC5D,OAAO;QACL,QAAQ,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,QAAQ;KACzF,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CACzB,OAAmC,EACnC,QAAkC,GAAG,EAAE,GAAE,CAAC,EAC1C,UAA8B,EAAE;IAEhC,+BAA+B,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AAC3D,CAAC;AAED,mGAAmG;AACnG,0DAA0D;AAC1D,KAAK,UAAU,+BAA+B,CAC5C,OAAmC,EACnC,KAA+B,EAC/B,OAA2B;IAE3B,IAAI,CAAC;QACH,MAAM,YAAY,CAAC,uBAAuB,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,uBAAuB,CAAC,yBAAyB,CAAC,OAAO,CAAC,CAAC,CAAC;QAC9F,OAAO,CAAC,MAAM,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,KAAK,CAAC,CAAC,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,gDAAgD;AAChD,SAAS,aAAa,CACpB,OAAmC,EACnC,KAA+B,EAC/B,OAA2B;IAE3B,MAAM,OAAO,GAAG,kBAAkB,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAE7D,YAAY,CAAC,sBAAsB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,GAAQ,EAAE,EAAE;QACvE,kBAAkB,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC/C,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,6CAA6C;AAC7C,SAAS,UAAU,CAAC,OAAe;IACjC,kBAAkB,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;AACjD,CAAC","sourcesContent":["import { Platform } from 'expo-modules-core';\n\nimport ExpoLocation from './ExpoLocation';\nimport { LocationObject, LocationAccuracy, LocationOptions } from './Location.types';\nimport { LocationSubscriber } from './LocationSubscribers';\n\ntype GeolocationSuccessCallback = (data: LocationObject) => void;\ntype GeolocationErrorCallback = (error: any) => void;\n\ntype GeolocationOptions = {\n enableHighAccuracy?: boolean;\n};\n\ndeclare const global: any;\n\n// @needsAudit\n/**\n * Polyfills `navigator.geolocation` for interop with the core React Native and Web API approach to geolocation.\n */\nexport function installWebGeolocationPolyfill(): void {\n if (Platform.OS !== 'web') {\n // Make sure `window.navigator` is defined in the global scope.\n if (!('window' in global)) {\n global.window = global;\n }\n if (!('navigator' in global.window)) {\n global.window.navigator = {};\n }\n\n // @ts-ignore\n window.navigator.geolocation = {\n getCurrentPosition,\n watchPosition,\n clearWatch,\n };\n }\n}\n\nfunction convertGeolocationOptions(options: GeolocationOptions): LocationOptions {\n return {\n accuracy: options.enableHighAccuracy ? LocationAccuracy.High : LocationAccuracy.Balanced,\n };\n}\n\nfunction getCurrentPosition(\n success: GeolocationSuccessCallback,\n error: GeolocationErrorCallback = () => {},\n options: GeolocationOptions = {}\n): void {\n _getCurrentPositionAsyncWrapper(success, error, options);\n}\n\n// This function exists to let us continue to return undefined from getCurrentPosition, while still\n// using async/await for the internal implementation of it\nasync function _getCurrentPositionAsyncWrapper(\n success: GeolocationSuccessCallback,\n error: GeolocationErrorCallback,\n options: GeolocationOptions\n): Promise<any> {\n try {\n await ExpoLocation.requestPermissionsAsync();\n const result = await ExpoLocation.getCurrentPositionAsync(convertGeolocationOptions(options));\n success(result);\n } catch (e) {\n error(e);\n }\n}\n\n// Polyfill: navigator.geolocation.watchPosition\nfunction watchPosition(\n success: GeolocationSuccessCallback,\n error: GeolocationErrorCallback,\n options: GeolocationOptions\n) {\n const watchId = LocationSubscriber.registerCallback(success);\n\n ExpoLocation.watchPositionImplAsync(watchId, options).catch((err: any) => {\n LocationSubscriber.unregisterCallback(watchId);\n error({ watchId, message: err.message, code: err.code });\n });\n\n return watchId;\n}\n\n// Polyfill: navigator.geolocation.clearWatch\nfunction clearWatch(watchId: number) {\n LocationSubscriber.unregisterCallback(watchId);\n}\n"]}
+248
View File
@@ -0,0 +1,248 @@
import { PermissionResponse } from 'expo-modules-core';
import { LocationErrorCallback, LocationCallback, LocationGeocodedAddress, LocationGeocodedLocation, LocationHeadingCallback, LocationHeadingObject, LocationLastKnownOptions, LocationObject, LocationOptions, LocationPermissionResponse, LocationProviderStatus, LocationRegion, LocationSubscription, LocationTaskOptions } from './Location.types';
/**
* Check status of location providers.
* @return A promise which fulfills with an object of type [`LocationProviderStatus`](#locationproviderstatus).
*/
export declare function getProviderStatusAsync(): Promise<LocationProviderStatus>;
/**
* Asks the user to turn on high accuracy location mode which enables network provider that uses
* Google Play services to improve location accuracy and location-based services.
* @return A promise resolving as soon as the user accepts the dialog. Rejects if denied.
*
* @platform android
*/
export declare function enableNetworkProviderAsync(): Promise<void>;
/**
* Requests for one-time delivery of the user's current location.
* Depending on given `accuracy` option it may take some time to resolve,
* especially when you're inside a building.
* > __Note:__ Calling it causes the location manager to obtain a location fix which may take several
* > seconds. Consider using [`getLastKnownPositionAsync`](#locationgetlastknownpositionasyncoptions)
* > if you expect to get a quick response and high accuracy is not required.
* @param options
* @return A promise which fulfills with an object of type [`LocationObject`](#locationobject).
*/
export declare function getCurrentPositionAsync(options?: LocationOptions): Promise<LocationObject>;
/**
* Gets the last known position of the device or `null` if it's not available or doesn't match given
* requirements such as maximum age or required accuracy.
* It's considered to be faster than `getCurrentPositionAsync` as it doesn't request for the current
* location, but keep in mind the returned location may not be up-to-date.
* @param options
* @return A promise which fulfills with an object of type [`LocationObject`](#locationobject) or
* `null` if it's not available or doesn't match given requirements such as maximum age or required
* accuracy.
*/
export declare function getLastKnownPositionAsync(options?: LocationLastKnownOptions): Promise<LocationObject | null>;
/**
* Subscribe to location updates from the device. Updates will only occur while the application is in
* the foreground. To get location updates while in background you'll need to use
* [`startLocationUpdatesAsync`](#locationstartlocationupdatesasynctaskname-options).
* @param options
* @param callback This function is called on each location update. It receives an object of type
* [`LocationObject`](#locationobject) as the first argument.
* @param errorHandler This function is called when an error occurs. It receives a string with the
* error message as the first argument.
* @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
*/
export declare function watchPositionAsync(options: LocationOptions, callback: LocationCallback, errorHandler?: LocationErrorCallback): Promise<LocationSubscription>;
/**
* Gets the current heading information from the device. To simplify, it calls `watchHeadingAsync`
* and waits for a couple of updates, and then returns the one that is accurate enough.
* @return A promise which fulfills with an object of type [`LocationHeadingObject`](#locationheadingobject).
*/
export declare function getHeadingAsync(): Promise<LocationHeadingObject>;
/**
* Subscribe to compass updates from the device.
*
* @param callback This function is called on each compass update. It receives an object of type
* [LocationHeadingObject](#locationheadingobject) as the first argument.
* @param errorHandler This function is called when an error occurs. It receives a string with the
* error message as the first argument.
* @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
*
* @platform android
* @platform ios
*/
export declare function watchHeadingAsync(callback: LocationHeadingCallback, errorHandler?: LocationErrorCallback): Promise<LocationSubscription>;
/**
* Geocode an address string to latitude-longitude location.
*
* On Android, you must request location permissions with [`requestForegroundPermissionsAsync`](#locationrequestforegroundpermissionsasync)
* before geocoding can be used.
*
* > **Note**: Geocoding is resource consuming and has to be used reasonably. Creating too many
* > requests at a time can result in an error, so they have to be managed properly.
* > It's also discouraged to use geocoding while the app is in the background and its results won't
* > be shown to the user immediately.
*
* @param address A string representing address, eg. `"Baker Street London"`.
* @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedLocation`](#locationgeocodedlocation)
* objects.
*
* @platform android
* @platform ios
*/
export declare function geocodeAsync(address: string): Promise<LocationGeocodedLocation[]>;
/**
* Reverse geocode a location to postal address.
*
* On Android, you must request location permissions with [`requestForegroundPermissionsAsync`](#locationrequestforegroundpermissionsasync)
* before geocoding can be used.
*
* > **Note**: Geocoding is resource consuming and has to be used reasonably. Creating too many
* > requests at a time can result in an error, so they have to be managed properly.
* > It's also discouraged to use geocoding while the app is in the background and its results won't
* > be shown to the user immediately.
*
* @param location An object representing a location.
* @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedAddress`](#locationgeocodedaddress) objects.
*
* @platform android
* @platform ios
*/
export declare function reverseGeocodeAsync(location: Pick<LocationGeocodedLocation, 'latitude' | 'longitude'>): Promise<LocationGeocodedAddress[]>;
/**
* Checks user's permissions for accessing location while the app is in the foreground.
* @return A promise that fulfills with an object of type [`LocationPermissionResponse`](#locationpermissionresponse).
*/
export declare function getForegroundPermissionsAsync(): Promise<LocationPermissionResponse>;
/**
* Asks the user to grant permissions for location while the app is in the foreground.
* @return A promise that fulfills with an object of type [`LocationPermissionResponse`](#locationpermissionresponse).
*/
export declare function requestForegroundPermissionsAsync(): Promise<LocationPermissionResponse>;
/**
* Check or request permissions for the foreground location.
* This uses both `requestForegroundPermissionsAsync` and `getForegroundPermissionsAsync` to interact with the permissions.
*
* @example
* ```ts
* const [status, requestPermission] = Location.useForegroundPermissions();
* ```
*/
export declare const useForegroundPermissions: (options?: import("expo-modules-core").PermissionHookOptions<object> | undefined) => [LocationPermissionResponse | null, () => Promise<LocationPermissionResponse>, () => Promise<LocationPermissionResponse>];
/**
* Checks user's permissions for accessing location while the app is in the background.
* @return A promise that fulfills with an object of type [`PermissionResponse`](#permissionresponse).
*/
export declare function getBackgroundPermissionsAsync(): Promise<PermissionResponse>;
/**
* Asks the user to grant permissions for location while the app is in the background.
* On __Android 11 or higher__: this method will open the system settings page - before that happens
* you should explain to the user why your application needs background location permission.
* For example, you can use `Modal` component from `react-native` to do that.
* > __Note__: Foreground permissions should be granted before asking for the background permissions
* (your app can't obtain background permission without foreground permission).
* @return A promise that fulfills with an object of type [`PermissionResponse`](#permissionresponse).
*/
export declare function requestBackgroundPermissionsAsync(): Promise<PermissionResponse>;
/**
* Check or request permissions for the background location.
* This uses both `requestBackgroundPermissionsAsync` and `getBackgroundPermissionsAsync` to
* interact with the permissions.
*
* @example
* ```ts
* const [status, requestPermission] = Location.useBackgroundPermissions();
* ```
*/
export declare const useBackgroundPermissions: (options?: import("expo-modules-core").PermissionHookOptions<object> | undefined) => [PermissionResponse | null, () => Promise<PermissionResponse>, () => Promise<PermissionResponse>];
/**
* Checks whether location services are enabled by the user.
* @return A promise which fulfills to `true` if location services are enabled on the device,
* or `false` if not.
*/
export declare function hasServicesEnabledAsync(): Promise<boolean>;
export declare function isBackgroundLocationAvailableAsync(): Promise<boolean>;
/**
* Registers for receiving location updates that can also come when the app is in the background.
*
* # Task parameters
*
* Background location task will be receiving following data:
* - `locations` - An array of the new locations.
*
* @example
* ```ts
* import * as TaskManager from 'expo-task-manager';
*
* TaskManager.defineTask(YOUR_TASK_NAME, ({ data: { locations }, error }) => {
* if (error) {
* // check `error.message` for more details.
* return;
* }
* console.log('Received new locations', locations);
* });
* ```
*
* @param taskName Name of the task receiving location updates.
* @param options An object of options passed to the location manager.
*
* @return A promise resolving once the task with location updates is registered.
*/
export declare function startLocationUpdatesAsync(taskName: string, options?: LocationTaskOptions): Promise<void>;
/**
* Stops location updates for specified task.
* @param taskName Name of the background location task to stop.
* @return A promise resolving as soon as the task is unregistered.
*/
export declare function stopLocationUpdatesAsync(taskName: string): Promise<void>;
/**
* @param taskName Name of the location task to check.
* @return A promise which fulfills with boolean value indicating whether the location task is
* started or not.
*/
export declare function hasStartedLocationUpdatesAsync(taskName: string): Promise<boolean>;
/**
* Starts geofencing for given regions. When the new event comes, the task with specified name will
* be called with the region that the device enter to or exit from.
* If you want to add or remove regions from already running geofencing task, you can just call
* `startGeofencingAsync` again with the new array of regions.
*
* # Task parameters
*
* Geofencing task will be receiving following data:
* - `eventType` - Indicates the reason for calling the task, which can be triggered by entering or exiting the region.
* See [`GeofencingEventType`](#geofencingeventtype).
* - `region` - Object containing details about updated region. See [`LocationRegion`](#locationregion) for more details.
*
* @param taskName Name of the task that will be called when the device enters or exits from specified regions.
* @param regions Array of region objects to be geofenced.
*
* @return A promise resolving as soon as the task is registered.
*
* @example
* ```ts
* import { GeofencingEventType } from 'expo-location';
* import * as TaskManager from 'expo-task-manager';
*
* TaskManager.defineTask(YOUR_TASK_NAME, ({ data: { eventType, region }, error }) => {
* if (error) {
* // check `error.message` for more details.
* return;
* }
* if (eventType === GeofencingEventType.Enter) {
* console.log("You've entered region:", region);
* } else if (eventType === GeofencingEventType.Exit) {
* console.log("You've left region:", region);
* }
* });
* ```
*/
export declare function startGeofencingAsync(taskName: string, regions?: LocationRegion[]): Promise<void>;
/**
* Stops geofencing for specified task. It unregisters the background task so the app will not be
* receiving any updates, especially in the background.
* @param taskName Name of the task to unregister.
* @return A promise resolving as soon as the task is unregistered.
*/
export declare function stopGeofencingAsync(taskName: string): Promise<void>;
/**
* @param taskName Name of the geofencing task to check.
* @return A promise which fulfills with boolean value indicating whether the geofencing task is
* started or not.
*/
export declare function hasStartedGeofencingAsync(taskName: string): Promise<boolean>;
//# sourceMappingURL=Location.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Location.d.ts","sourceRoot":"","sources":["../src/Location.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAkC,MAAM,mBAAmB,CAAC;AAGvF,OAAO,EACL,qBAAqB,EAErB,gBAAgB,EAChB,uBAAuB,EACvB,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,cAAc,EACd,eAAe,EACf,0BAA0B,EAC1B,sBAAsB,EACtB,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAW1B;;;GAGG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAE9E;AAGD;;;;;;GAMG;AACH,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC,CAShE;AAGD;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAEzB;AAGD;;;;;;;;;GASG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAEhC;AAGD;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,CAAC,EAAE,qBAAqB,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAY/B;AAGD;;;;GAIG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAwBtE;AAGD;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,uBAAuB,EACjC,YAAY,CAAC,EAAE,qBAAqB,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAY/B;AAGD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,EAAE,CAAC,CAgBvF;AAGD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,IAAI,CAAC,wBAAwB,EAAE,UAAU,GAAG,WAAW,CAAC,GACjE,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAkBpC;AAGD;;;GAGG;AACH,wBAAsB,6BAA6B,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAEzF;AAGD;;;GAGG;AACH,wBAAsB,iCAAiC,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAE7F;AAGD;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,gNAGnC,CAAC;AAGH;;;GAGG;AACH,wBAAsB,6BAA6B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAEjF;AAGD;;;;;;;;GAQG;AACH,wBAAsB,iCAAiC,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAErF;AAGD;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,wLAGnC,CAAC;AAKH;;;;GAIG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC,CAEhE;AAsBD,wBAAsB,kCAAkC,IAAI,OAAO,CAAC,OAAO,CAAC,CAG3E;AAGD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,mBAA6D,GACrE,OAAO,CAAC,IAAI,CAAC,CAGf;AAGD;;;;GAIG;AACH,wBAAsB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;AAGD;;;;GAIG;AACH,wBAAsB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGvF;AA0BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,cAAc,EAAO,GAC7B,OAAO,CAAC,IAAI,CAAC,CAIf;AAGD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzE;AAGD;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGlF"}
+428
View File
@@ -0,0 +1,428 @@
import { isRunningInExpoGo } from 'expo';
import { createPermissionHook, Platform } from 'expo-modules-core';
import ExpoLocation from './ExpoLocation';
import { LocationAccuracy, } from './Location.types';
import { LocationSubscriber, HeadingSubscriber, LocationErrorSubscriber, } from './LocationSubscribers';
// Flag for warning about background services not being available in Expo Go
let warnAboutExpoGoDisplayed = false;
// @needsAudit
/**
* Check status of location providers.
* @return A promise which fulfills with an object of type [`LocationProviderStatus`](#locationproviderstatus).
*/
export async function getProviderStatusAsync() {
return ExpoLocation.getProviderStatusAsync();
}
// @needsAudit
/**
* Asks the user to turn on high accuracy location mode which enables network provider that uses
* Google Play services to improve location accuracy and location-based services.
* @return A promise resolving as soon as the user accepts the dialog. Rejects if denied.
*
* @platform android
*/
export async function enableNetworkProviderAsync() {
// If network provider is disabled (user's location mode is set to "Device only"),
// Android's location provider may not give you any results. Use this method in order to ask the user
// to change the location mode to "High accuracy" which uses Google Play services and enables network provider.
// `getCurrentPositionAsync` and `watchPositionAsync` are doing it automatically anyway.
if (Platform.OS === 'android') {
return ExpoLocation.enableNetworkProviderAsync();
}
}
// @needsAudit
/**
* Requests for one-time delivery of the user's current location.
* Depending on given `accuracy` option it may take some time to resolve,
* especially when you're inside a building.
* > __Note:__ Calling it causes the location manager to obtain a location fix which may take several
* > seconds. Consider using [`getLastKnownPositionAsync`](#locationgetlastknownpositionasyncoptions)
* > if you expect to get a quick response and high accuracy is not required.
* @param options
* @return A promise which fulfills with an object of type [`LocationObject`](#locationobject).
*/
export async function getCurrentPositionAsync(options = {}) {
return ExpoLocation.getCurrentPositionAsync(options);
}
// @needsAudit
/**
* Gets the last known position of the device or `null` if it's not available or doesn't match given
* requirements such as maximum age or required accuracy.
* It's considered to be faster than `getCurrentPositionAsync` as it doesn't request for the current
* location, but keep in mind the returned location may not be up-to-date.
* @param options
* @return A promise which fulfills with an object of type [`LocationObject`](#locationobject) or
* `null` if it's not available or doesn't match given requirements such as maximum age or required
* accuracy.
*/
export async function getLastKnownPositionAsync(options = {}) {
return ExpoLocation.getLastKnownPositionAsync(options);
}
// @needsAudit
/**
* Subscribe to location updates from the device. Updates will only occur while the application is in
* the foreground. To get location updates while in background you'll need to use
* [`startLocationUpdatesAsync`](#locationstartlocationupdatesasynctaskname-options).
* @param options
* @param callback This function is called on each location update. It receives an object of type
* [`LocationObject`](#locationobject) as the first argument.
* @param errorHandler This function is called when an error occurs. It receives a string with the
* error message as the first argument.
* @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
*/
export async function watchPositionAsync(options, callback, errorHandler) {
const watchId = LocationSubscriber.registerCallback(callback);
errorHandler && LocationErrorSubscriber.registerCallbackForId(watchId, errorHandler);
await ExpoLocation.watchPositionImplAsync(watchId, options);
return {
remove() {
LocationSubscriber.unregisterCallback(watchId);
errorHandler && LocationErrorSubscriber.unregisterCallback(watchId);
},
};
}
// @needsAudit
/**
* Gets the current heading information from the device. To simplify, it calls `watchHeadingAsync`
* and waits for a couple of updates, and then returns the one that is accurate enough.
* @return A promise which fulfills with an object of type [`LocationHeadingObject`](#locationheadingobject).
*/
export async function getHeadingAsync() {
return new Promise(async (resolve, reject) => {
let tries = 0;
let subscriber = undefined;
try {
subscriber = await watchHeadingAsync((heading) => {
if (heading.accuracy > 1 || tries > 5) {
subscriber?.remove();
resolve(heading);
}
else {
tries += 1;
}
}, (reason) => {
subscriber?.remove();
reject(reason);
});
}
catch (e) {
reject(e);
}
});
}
// @needsAudit
/**
* Subscribe to compass updates from the device.
*
* @param callback This function is called on each compass update. It receives an object of type
* [LocationHeadingObject](#locationheadingobject) as the first argument.
* @param errorHandler This function is called when an error occurs. It receives a string with the
* error message as the first argument.
* @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
*
* @platform android
* @platform ios
*/
export async function watchHeadingAsync(callback, errorHandler) {
const watchId = HeadingSubscriber.registerCallback(callback);
errorHandler && LocationErrorSubscriber.registerCallbackForId(watchId, errorHandler);
await ExpoLocation.watchDeviceHeading(watchId);
return {
remove() {
HeadingSubscriber.unregisterCallback(watchId);
errorHandler && LocationErrorSubscriber.unregisterCallback(watchId);
},
};
}
// @needsAudit
/**
* Geocode an address string to latitude-longitude location.
*
* On Android, you must request location permissions with [`requestForegroundPermissionsAsync`](#locationrequestforegroundpermissionsasync)
* before geocoding can be used.
*
* > **Note**: Geocoding is resource consuming and has to be used reasonably. Creating too many
* > requests at a time can result in an error, so they have to be managed properly.
* > It's also discouraged to use geocoding while the app is in the background and its results won't
* > be shown to the user immediately.
*
* @param address A string representing address, eg. `"Baker Street London"`.
* @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedLocation`](#locationgeocodedlocation)
* objects.
*
* @platform android
* @platform ios
*/
export async function geocodeAsync(address) {
if (typeof address !== 'string') {
throw new TypeError(`Address to geocode must be a string. Got ${address} instead.`);
}
if (Platform.OS === 'web') {
if (__DEV__) {
console.warn('The Geocoding API has been removed in SDK 49, use Place Autocomplete service instead' +
'(https://developers.google.com/maps/documentation/places/web-service/autocomplete)');
}
return [];
}
return await ExpoLocation.geocodeAsync(address);
}
// @needsAudit
/**
* Reverse geocode a location to postal address.
*
* On Android, you must request location permissions with [`requestForegroundPermissionsAsync`](#locationrequestforegroundpermissionsasync)
* before geocoding can be used.
*
* > **Note**: Geocoding is resource consuming and has to be used reasonably. Creating too many
* > requests at a time can result in an error, so they have to be managed properly.
* > It's also discouraged to use geocoding while the app is in the background and its results won't
* > be shown to the user immediately.
*
* @param location An object representing a location.
* @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedAddress`](#locationgeocodedaddress) objects.
*
* @platform android
* @platform ios
*/
export async function reverseGeocodeAsync(location) {
if (typeof location.latitude !== 'number' || typeof location.longitude !== 'number') {
throw new TypeError('Location to reverse-geocode must be an object with number properties `latitude` and `longitude`.');
}
if (Platform.OS === 'web') {
if (__DEV__) {
console.warn('The Geocoding API has been removed in SDK 49, use Place Autocomplete service instead' +
'(https://developers.google.com/maps/documentation/places/web-service/autocomplete)');
}
return [];
}
return await ExpoLocation.reverseGeocodeAsync(location);
}
// @needsAudit
/**
* Checks user's permissions for accessing location while the app is in the foreground.
* @return A promise that fulfills with an object of type [`LocationPermissionResponse`](#locationpermissionresponse).
*/
export async function getForegroundPermissionsAsync() {
return await ExpoLocation.getForegroundPermissionsAsync();
}
// @needsAudit
/**
* Asks the user to grant permissions for location while the app is in the foreground.
* @return A promise that fulfills with an object of type [`LocationPermissionResponse`](#locationpermissionresponse).
*/
export async function requestForegroundPermissionsAsync() {
return await ExpoLocation.requestForegroundPermissionsAsync();
}
// @needsAudit
/**
* Check or request permissions for the foreground location.
* This uses both `requestForegroundPermissionsAsync` and `getForegroundPermissionsAsync` to interact with the permissions.
*
* @example
* ```ts
* const [status, requestPermission] = Location.useForegroundPermissions();
* ```
*/
export const useForegroundPermissions = createPermissionHook({
getMethod: getForegroundPermissionsAsync,
requestMethod: requestForegroundPermissionsAsync,
});
// @needsAudit
/**
* Checks user's permissions for accessing location while the app is in the background.
* @return A promise that fulfills with an object of type [`PermissionResponse`](#permissionresponse).
*/
export async function getBackgroundPermissionsAsync() {
return await ExpoLocation.getBackgroundPermissionsAsync();
}
// @needsAudit
/**
* Asks the user to grant permissions for location while the app is in the background.
* On __Android 11 or higher__: this method will open the system settings page - before that happens
* you should explain to the user why your application needs background location permission.
* For example, you can use `Modal` component from `react-native` to do that.
* > __Note__: Foreground permissions should be granted before asking for the background permissions
* (your app can't obtain background permission without foreground permission).
* @return A promise that fulfills with an object of type [`PermissionResponse`](#permissionresponse).
*/
export async function requestBackgroundPermissionsAsync() {
return await ExpoLocation.requestBackgroundPermissionsAsync();
}
// @needsAudit
/**
* Check or request permissions for the background location.
* This uses both `requestBackgroundPermissionsAsync` and `getBackgroundPermissionsAsync` to
* interact with the permissions.
*
* @example
* ```ts
* const [status, requestPermission] = Location.useBackgroundPermissions();
* ```
*/
export const useBackgroundPermissions = createPermissionHook({
getMethod: getBackgroundPermissionsAsync,
requestMethod: requestBackgroundPermissionsAsync,
});
// --- Location service
// @needsAudit
/**
* Checks whether location services are enabled by the user.
* @return A promise which fulfills to `true` if location services are enabled on the device,
* or `false` if not.
*/
export async function hasServicesEnabledAsync() {
return await ExpoLocation.hasServicesEnabledAsync();
}
// --- Background location updates
function _validate(taskName) {
if (!taskName || typeof taskName !== 'string') {
throw new Error(`\`taskName\` must be a non-empty string. Got ${taskName} instead.`);
}
if (isRunningInExpoGo()) {
if (!warnAboutExpoGoDisplayed) {
const message = 'Background location is limited in Expo Go:\n' +
'On Android, it is not available at all.\n' +
'On iOS, it works when running in the Simulator.\n' +
'You can use this API, and all others, in a development build. Learn more: https://expo.fyi/dev-client.';
console.warn(message);
warnAboutExpoGoDisplayed = true;
}
}
}
// @docsMissing
export async function isBackgroundLocationAvailableAsync() {
const providerStatus = await getProviderStatusAsync();
return providerStatus.backgroundModeEnabled;
}
// @needsAudit
/**
* Registers for receiving location updates that can also come when the app is in the background.
*
* # Task parameters
*
* Background location task will be receiving following data:
* - `locations` - An array of the new locations.
*
* @example
* ```ts
* import * as TaskManager from 'expo-task-manager';
*
* TaskManager.defineTask(YOUR_TASK_NAME, ({ data: { locations }, error }) => {
* if (error) {
* // check `error.message` for more details.
* return;
* }
* console.log('Received new locations', locations);
* });
* ```
*
* @param taskName Name of the task receiving location updates.
* @param options An object of options passed to the location manager.
*
* @return A promise resolving once the task with location updates is registered.
*/
export async function startLocationUpdatesAsync(taskName, options = { accuracy: LocationAccuracy.Balanced }) {
_validate(taskName);
await ExpoLocation.startLocationUpdatesAsync(taskName, options);
}
// @needsAudit
/**
* Stops location updates for specified task.
* @param taskName Name of the background location task to stop.
* @return A promise resolving as soon as the task is unregistered.
*/
export async function stopLocationUpdatesAsync(taskName) {
_validate(taskName);
await ExpoLocation.stopLocationUpdatesAsync(taskName);
}
// @needsAudit
/**
* @param taskName Name of the location task to check.
* @return A promise which fulfills with boolean value indicating whether the location task is
* started or not.
*/
export async function hasStartedLocationUpdatesAsync(taskName) {
_validate(taskName);
return ExpoLocation.hasStartedLocationUpdatesAsync(taskName);
}
// --- Geofencing
function _validateRegions(regions) {
if (!regions || regions.length === 0) {
throw new Error('Regions array cannot be empty. Use `stopGeofencingAsync` if you want to stop geofencing all regions');
}
for (const region of regions) {
if (typeof region.latitude !== 'number') {
throw new TypeError(`Region's latitude must be a number. Got '${region.latitude}' instead.`);
}
if (typeof region.longitude !== 'number') {
throw new TypeError(`Region's longitude must be a number. Got '${region.longitude}' instead.`);
}
if (typeof region.radius !== 'number') {
throw new TypeError(`Region's radius must be a number. Got '${region.radius}' instead.`);
}
}
}
// @needsAudit
/**
* Starts geofencing for given regions. When the new event comes, the task with specified name will
* be called with the region that the device enter to or exit from.
* If you want to add or remove regions from already running geofencing task, you can just call
* `startGeofencingAsync` again with the new array of regions.
*
* # Task parameters
*
* Geofencing task will be receiving following data:
* - `eventType` - Indicates the reason for calling the task, which can be triggered by entering or exiting the region.
* See [`GeofencingEventType`](#geofencingeventtype).
* - `region` - Object containing details about updated region. See [`LocationRegion`](#locationregion) for more details.
*
* @param taskName Name of the task that will be called when the device enters or exits from specified regions.
* @param regions Array of region objects to be geofenced.
*
* @return A promise resolving as soon as the task is registered.
*
* @example
* ```ts
* import { GeofencingEventType } from 'expo-location';
* import * as TaskManager from 'expo-task-manager';
*
* TaskManager.defineTask(YOUR_TASK_NAME, ({ data: { eventType, region }, error }) => {
* if (error) {
* // check `error.message` for more details.
* return;
* }
* if (eventType === GeofencingEventType.Enter) {
* console.log("You've entered region:", region);
* } else if (eventType === GeofencingEventType.Exit) {
* console.log("You've left region:", region);
* }
* });
* ```
*/
export async function startGeofencingAsync(taskName, regions = []) {
_validate(taskName);
_validateRegions(regions);
await ExpoLocation.startGeofencingAsync(taskName, { regions });
}
// @needsAudit
/**
* Stops geofencing for specified task. It unregisters the background task so the app will not be
* receiving any updates, especially in the background.
* @param taskName Name of the task to unregister.
* @return A promise resolving as soon as the task is unregistered.
*/
export async function stopGeofencingAsync(taskName) {
_validate(taskName);
await ExpoLocation.stopGeofencingAsync(taskName);
}
// @needsAudit
/**
* @param taskName Name of the geofencing task to check.
* @return A promise which fulfills with boolean value indicating whether the geofencing task is
* started or not.
*/
export async function hasStartedGeofencingAsync(taskName) {
_validate(taskName);
return ExpoLocation.hasStartedGeofencingAsync(taskName);
}
//# sourceMappingURL=Location.js.map
File diff suppressed because one or more lines are too long
+462
View File
@@ -0,0 +1,462 @@
import { PermissionResponse } from 'expo-modules-core';
/**
* Enum with available location accuracies.
*/
export declare enum LocationAccuracy {
/**
* Accurate to the nearest three kilometers.
*/
Lowest = 1,
/**
* Accurate to the nearest kilometer.
*/
Low = 2,
/**
* Accurate to within one hundred meters.
*/
Balanced = 3,
/**
* Accurate to within ten meters of the desired target.
*/
High = 4,
/**
* The best level of accuracy available.
*/
Highest = 5,
/**
* The highest possible accuracy that uses additional sensor data to facilitate navigation apps.
*/
BestForNavigation = 6
}
/**
* Enum with available activity types of background location tracking.
*/
export declare enum LocationActivityType {
/**
* Default activity type. Use it if there is no other type that matches the activity you track.
*/
Other = 1,
/**
* Location updates are being used specifically during vehicular navigation to track location
* changes to the automobile.
*/
AutomotiveNavigation = 2,
/**
* Use this activity type if you track fitness activities such as walking, running, cycling,
* and so on.
*/
Fitness = 3,
/**
* Activity type for movements for other types of vehicular navigation that are not automobile
* related.
*/
OtherNavigation = 4,
/**
* Intended for airborne activities. Fall backs to `ActivityType.Other` if
* unsupported.
* @platform ios
*/
Airborne = 5
}
/**
* A type of the event that geofencing task can receive.
*/
export declare enum LocationGeofencingEventType {
/**
* Emitted when the device entered observed region.
*/
Enter = 1,
/**
* Occurs as soon as the device left observed region
*/
Exit = 2
}
/**
* State of the geofencing region that you receive through the geofencing task.
*/
export declare enum LocationGeofencingRegionState {
/**
* Indicates that the device position related to the region is unknown.
*/
Unknown = 0,
/**
* Indicates that the device is inside the region.
*/
Inside = 1,
/**
* Inverse of inside state.
*/
Outside = 2
}
/**
* Type representing options argument in `getCurrentPositionAsync`.
*/
export type LocationOptions = {
/**
* Location manager accuracy. Pass one of `Accuracy` enum values.
* For low-accuracies the implementation can avoid geolocation providers
* that consume a significant amount of power (such as GPS).
*
* @default LocationAccuracy.Balanced
*/
accuracy?: LocationAccuracy;
/**
* Specifies whether to ask the user to turn on improved accuracy location mode
* which uses Wi-Fi, cell networks and GPS sensor.
* @default true
* @platform android
*/
mayShowUserSettingsDialog?: boolean;
/**
* Minimum time to wait between each update in milliseconds.
* Default value may depend on `accuracy` option.
* @platform android
*/
timeInterval?: number;
/**
* Receive updates only when the location has changed by at least this distance in meters.
* Default value may depend on `accuracy` option.
*/
distanceInterval?: number;
};
/**
* Type representing options object that can be passed to `getLastKnownPositionAsync`.
*/
export type LocationLastKnownOptions = {
/**
* A number of milliseconds after which the last known location starts to be invalid and thus
* `null` is returned.
*/
maxAge?: number;
/**
* The maximum radius of uncertainty for the location, measured in meters. If the last known
* location's accuracy radius is bigger (less accurate) then `null` is returned.
*/
requiredAccuracy?: number;
};
/**
* Type representing background location task options.
*/
export type LocationTaskOptions = LocationOptions & {
/**
* A boolean indicating whether the status bar changes its appearance when
* location services are used in the background.
* @default false
* @platform ios
*/
showsBackgroundLocationIndicator?: boolean;
/**
* The distance in meters that must occur between last reported location and the current location
* before deferred locations are reported.
* @default 0
*/
deferredUpdatesDistance?: number;
deferredUpdatesTimeout?: number;
/**
* Minimum time interval in milliseconds that must pass since last reported location before all
* later locations are reported in a batched update
* @default 0
*/
deferredUpdatesInterval?: number;
/**
* The type of user activity associated with the location updates.
* @see See [Apple docs](https://developer.apple.com/documentation/corelocation/cllocationmanager/1620567-activitytype) for more details.
* @default ActivityType.Other
* @platform ios
*/
activityType?: LocationActivityType;
/**
* A boolean value indicating whether the location manager can pause location
* updates to improve battery life without sacrificing location data. When this option is set to
* `true`, the location manager pauses updates (and powers down the appropriate hardware) at times
* when the location data is unlikely to change. You can help the determination of when to pause
* location updates by assigning a value to the `activityType` property.
* @default false
* @platform ios
*/
pausesUpdatesAutomatically?: boolean;
foregroundService?: LocationTaskServiceOptions;
};
export type LocationTaskServiceOptions = {
/**
* Title of the foreground service notification.
*/
notificationTitle: string;
/**
* Subtitle of the foreground service notification.
*/
notificationBody: string;
/**
* Color of the foreground service notification. Accepts `#RRGGBB` and `#AARRGGBB` hex formats.
*/
notificationColor?: string;
/**
* Boolean value whether to destroy the foreground service if the app is killed.
*/
killServiceOnDestroy?: boolean;
};
/**
* Type representing geofencing region object.
*/
export type LocationRegion = {
/**
* The identifier of the region object. Defaults to auto-generated UUID hash.
*/
identifier?: string;
/**
* The latitude in degrees of region's center point.
*/
latitude: number;
/**
* The longitude in degrees of region's center point.
*/
longitude: number;
/**
* The radius measured in meters that defines the region's outer boundary.
*/
radius: number;
/**
* Boolean value whether to call the task if the device enters the region.
* @default true
*/
notifyOnEnter?: boolean;
/**
* Boolean value whether to call the task if the device exits the region.
* @default true
*/
notifyOnExit?: boolean;
/**
* One of [GeofencingRegionState](#geofencingregionstate) region state. Determines whether the
* device is inside or outside a region.
*/
state?: LocationGeofencingRegionState;
};
/**
* Type representing the location object.
*/
export type LocationObject = {
/**
* The coordinates of the position.
*/
coords: LocationObjectCoords;
/**
* The time at which this position information was obtained, in milliseconds since epoch.
*/
timestamp: number;
/**
* Whether the location coordinates is mocked or not.
* @platform android
*/
mocked?: boolean;
};
/**
* Type representing the location GPS related data.
*/
export type LocationObjectCoords = {
/**
* The latitude in degrees.
*/
latitude: number;
/**
* The longitude in degrees.
*/
longitude: number;
/**
* The altitude in meters above the WGS 84 reference ellipsoid. Can be `null` on Web if it's not available.
*/
altitude: number | null;
/**
* The radius of uncertainty for the location, measured in meters. Can be `null` on Web if it's not available.
*/
accuracy: number | null;
/**
* The accuracy of the altitude value, in meters. Can be `null` on Web if it's not available.
*/
altitudeAccuracy: number | null;
/**
* Horizontal direction of travel of this device, measured in degrees starting at due north and
* continuing clockwise around the compass. Thus, north is 0 degrees, east is 90 degrees, south is
* 180 degrees, and so on. Can be `null` on Web if it's not available.
*/
heading: number | null;
/**
* The instantaneous speed of the device in meters per second. Can be `null` on Web if it's not available.
*/
speed: number | null;
};
/**
* Represents `watchPositionAsync` callback.
*/
export type LocationCallback = (location: LocationObject) => any;
/**
* Error callback for location methods.
*/
export type LocationErrorCallback = (reason: string) => void;
/**
* Represents the object containing details about location provider.
*/
export type LocationProviderStatus = {
/**
* Whether location services are enabled. See [Location.hasServicesEnabledAsync](#locationhasservicesenabledasync)
* for a more convenient solution to get this value.
*/
locationServicesEnabled: boolean;
backgroundModeEnabled: boolean;
/**
* Whether the GPS provider is available. If `true` the location data will come
* from GPS, especially for requests with high accuracy.
* @platform android
*/
gpsAvailable?: boolean;
/**
* Whether the network provider is available. If `true` the location data will
* come from cellular network, especially for requests with low accuracy.
* @platform android
*/
networkAvailable?: boolean;
/**
* Whether the passive provider is available. If `true` the location data will
* be determined passively.
* @platform android
*/
passiveAvailable?: boolean;
};
/**
* Type of the object containing heading details and provided by `watchHeadingAsync` callback.
*/
export type LocationHeadingObject = {
/**
* Measure of true north in degrees (needs location permissions, will return `-1` if not given).
*/
trueHeading: number;
/**
* Measure of magnetic north in degrees.
*/
magHeading: number;
/**
* Level of calibration of compass:
* - `3`: high accuracy
* - `2`: medium accuracy
* - `1`: low accuracy
* - `0`: none
*
* Reference for iOS:
* - `3`: < 20 degrees uncertainty
* - `2`: < 35 degrees
* - `1`: < 50 degrees
* - `0`: > 50 degrees
*/
accuracy: number;
};
/**
* Represents `watchHeadingAsync` callback.
*/
export type LocationHeadingCallback = (location: LocationHeadingObject) => any;
/**
* Type representing a result of `geocodeAsync`.
*/
export type LocationGeocodedLocation = {
/**
* The latitude in degrees.
*/
latitude: number;
/**
* The longitude in degrees.
*/
longitude: number;
/**
* The altitude in meters above the WGS 84 reference ellipsoid.
*/
altitude?: number;
/**
* The radius of uncertainty for the location, measured in meters.
*/
accuracy?: number;
};
/**
* Type representing a result of `reverseGeocodeAsync`.
*/
export type LocationGeocodedAddress = {
/**
* City name of the address.
*/
city: string | null;
/**
* Additional city-level information like district name.
*/
district: string | null;
/**
* Street number of the address.
*/
streetNumber: string | null;
/**
* Street name of the address.
*/
street: string | null;
/**
* The state or province associated with the address.
*/
region: string | null;
/**
* Additional information about administrative area.
*/
subregion: string | null;
/**
* Localized country name of the address.
*/
country: string | null;
/**
* Postal code of the address.
*/
postalCode: string | null;
/**
* The name of the placemark, for example, "Tower Bridge".
*/
name: string | null;
/**
* Localized (ISO) country code of the address, if available.
*/
isoCountryCode: string | null;
/**
* The timezone identifier associated with the address.
* @platform ios
*/
timezone: string | null;
/**
* Composed string of the address components, for example, "111 8th Avenue, New York, NY".
* @platform android
*/
formattedAddress: string | null;
};
/**
* Represents subscription object returned by methods watching for new locations or headings.
*/
export type LocationSubscription = {
/**
* Call this function with no arguments to remove this subscription. The callback will no longer
* be called for location updates.
*/
remove: () => void;
};
export type PermissionDetailsLocationIOS = {
/**
* The scope of granted permission. Indicates when it's possible to use location.
*/
scope: 'whenInUse' | 'always' | 'none';
};
export type PermissionDetailsLocationAndroid = {
/**
* Indicates the type of location provider.
*/
accuracy: 'fine' | 'coarse' | 'none';
};
/**
* `LocationPermissionResponse` extends [`PermissionResponse`](#permissionresponse)
* type exported by `expo-modules-core` and contains additional platform-specific fields.
*/
export type LocationPermissionResponse = PermissionResponse & {
ios?: PermissionDetailsLocationIOS;
android?: PermissionDetailsLocationAndroid;
};
export type { PermissionResponse };
//# sourceMappingURL=Location.types.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"Location.types.d.ts","sourceRoot":"","sources":["../src/Location.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGvD;;GAEG;AACH,oBAAY,gBAAgB;IAC1B;;OAEG;IACH,MAAM,IAAI;IACV;;OAEG;IACH,GAAG,IAAI;IACP;;OAEG;IACH,QAAQ,IAAI;IACZ;;OAEG;IACH,IAAI,IAAI;IACR;;OAEG;IACH,OAAO,IAAI;IACX;;OAEG;IACH,iBAAiB,IAAI;CACtB;AAGD;;GAEG;AACH,oBAAY,oBAAoB;IAC9B;;OAEG;IACH,KAAK,IAAI;IACT;;;OAGG;IACH,oBAAoB,IAAI;IACxB;;;OAGG;IACH,OAAO,IAAI;IACX;;;OAGG;IACH,eAAe,IAAI;IACnB;;;;OAIG;IACH,QAAQ,IAAI;CACb;AAGD;;GAEG;AACH,oBAAY,2BAA2B;IACrC;;OAEG;IACH,KAAK,IAAI;IACT;;OAEG;IACH,IAAI,IAAI;CACT;AAGD;;GAEG;AACH,oBAAY,6BAA6B;IACvC;;OAEG;IACH,OAAO,IAAI;IACX;;OAEG;IACH,MAAM,IAAI;IACV;;OAEG;IACH,OAAO,IAAI;CACZ;AAGD;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,eAAe,GAAG;IAClD;;;;;OAKG;IACH,gCAAgC,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAEjC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC;;;;;;;;OAQG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,iBAAiB,CAAC,EAAE,0BAA0B,CAAC;CAChD,CAAC;AAGF,MAAM,MAAM,0BAA0B,GAAG;IACvC;;OAEG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;OAEG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;OAEG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,KAAK,CAAC,EAAE,6BAA6B,CAAC;CACvC,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;OAEG;IACH,MAAM,EAAE,oBAAoB,CAAC;IAC7B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;OAEG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;OAEG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;OAEG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,cAAc,KAAK,GAAG,CAAC;AAEjE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAG7D;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;OAGG;IACH,uBAAuB,EAAE,OAAO,CAAC;IAEjC,qBAAqB,EAAE,OAAO,CAAC;IAC/B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,QAAQ,EAAE,qBAAqB,KAAK,GAAG,CAAC;AAG/E;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC;;OAEG;IACH,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB;;OAEG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;OAEG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;OAEG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;OAEG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;OAEG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;OAEG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;OAEG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;OAEG;IACH,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB;;OAEG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;OAGG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAGF,MAAM,MAAM,4BAA4B,GAAG;IACzC;;OAEG;IACH,KAAK,EAAE,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;CACxC,CAAC;AAGF,MAAM,MAAM,gCAAgC,GAAG;IAC7C;;OAEG;IACH,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;CACtC,CAAC;AAGF;;;GAGG;AACH,MAAM,MAAM,0BAA0B,GAAG,kBAAkB,GAAG;IAC5D,GAAG,CAAC,EAAE,4BAA4B,CAAC;IACnC,OAAO,CAAC,EAAE,gCAAgC,CAAC;CAC5C,CAAC;AAEF,YAAY,EAAE,kBAAkB,EAAE,CAAC"}
+98
View File
@@ -0,0 +1,98 @@
// @needsAudit
/**
* Enum with available location accuracies.
*/
export var LocationAccuracy;
(function (LocationAccuracy) {
/**
* Accurate to the nearest three kilometers.
*/
LocationAccuracy[LocationAccuracy["Lowest"] = 1] = "Lowest";
/**
* Accurate to the nearest kilometer.
*/
LocationAccuracy[LocationAccuracy["Low"] = 2] = "Low";
/**
* Accurate to within one hundred meters.
*/
LocationAccuracy[LocationAccuracy["Balanced"] = 3] = "Balanced";
/**
* Accurate to within ten meters of the desired target.
*/
LocationAccuracy[LocationAccuracy["High"] = 4] = "High";
/**
* The best level of accuracy available.
*/
LocationAccuracy[LocationAccuracy["Highest"] = 5] = "Highest";
/**
* The highest possible accuracy that uses additional sensor data to facilitate navigation apps.
*/
LocationAccuracy[LocationAccuracy["BestForNavigation"] = 6] = "BestForNavigation";
})(LocationAccuracy || (LocationAccuracy = {}));
// @needsAudit
/**
* Enum with available activity types of background location tracking.
*/
export var LocationActivityType;
(function (LocationActivityType) {
/**
* Default activity type. Use it if there is no other type that matches the activity you track.
*/
LocationActivityType[LocationActivityType["Other"] = 1] = "Other";
/**
* Location updates are being used specifically during vehicular navigation to track location
* changes to the automobile.
*/
LocationActivityType[LocationActivityType["AutomotiveNavigation"] = 2] = "AutomotiveNavigation";
/**
* Use this activity type if you track fitness activities such as walking, running, cycling,
* and so on.
*/
LocationActivityType[LocationActivityType["Fitness"] = 3] = "Fitness";
/**
* Activity type for movements for other types of vehicular navigation that are not automobile
* related.
*/
LocationActivityType[LocationActivityType["OtherNavigation"] = 4] = "OtherNavigation";
/**
* Intended for airborne activities. Fall backs to `ActivityType.Other` if
* unsupported.
* @platform ios
*/
LocationActivityType[LocationActivityType["Airborne"] = 5] = "Airborne";
})(LocationActivityType || (LocationActivityType = {}));
// @needsAudit
/**
* A type of the event that geofencing task can receive.
*/
export var LocationGeofencingEventType;
(function (LocationGeofencingEventType) {
/**
* Emitted when the device entered observed region.
*/
LocationGeofencingEventType[LocationGeofencingEventType["Enter"] = 1] = "Enter";
/**
* Occurs as soon as the device left observed region
*/
LocationGeofencingEventType[LocationGeofencingEventType["Exit"] = 2] = "Exit";
})(LocationGeofencingEventType || (LocationGeofencingEventType = {}));
// @needsAudit
/**
* State of the geofencing region that you receive through the geofencing task.
*/
export var LocationGeofencingRegionState;
(function (LocationGeofencingRegionState) {
/**
* Indicates that the device position related to the region is unknown.
*/
LocationGeofencingRegionState[LocationGeofencingRegionState["Unknown"] = 0] = "Unknown";
/**
* Indicates that the device is inside the region.
*/
LocationGeofencingRegionState[LocationGeofencingRegionState["Inside"] = 1] = "Inside";
/**
* Inverse of inside state.
*/
LocationGeofencingRegionState[LocationGeofencingRegionState["Outside"] = 2] = "Outside";
})(LocationGeofencingRegionState || (LocationGeofencingRegionState = {}));
//# sourceMappingURL=Location.types.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
import { LegacyEventEmitter } from 'expo-modules-core';
export declare const LocationEventEmitter: LegacyEventEmitter;
//# sourceMappingURL=LocationEventEmitter.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"LocationEventEmitter.d.ts","sourceRoot":"","sources":["../src/LocationEventEmitter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAIvD,eAAO,MAAM,oBAAoB,oBAAuC,CAAC"}
@@ -0,0 +1,4 @@
import { LegacyEventEmitter } from 'expo-modules-core';
import ExpoLocation from './ExpoLocation';
export const LocationEventEmitter = new LegacyEventEmitter(ExpoLocation);
//# sourceMappingURL=LocationEventEmitter.js.map
@@ -0,0 +1 @@
{"version":3,"file":"LocationEventEmitter.js","sourceRoot":"","sources":["../src/LocationEventEmitter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAE1C,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,kBAAkB,CAAC,YAAY,CAAC,CAAC","sourcesContent":["import { LegacyEventEmitter } from 'expo-modules-core';\n\nimport ExpoLocation from './ExpoLocation';\n\nexport const LocationEventEmitter = new LegacyEventEmitter(ExpoLocation);\n"]}
@@ -0,0 +1,2 @@
export declare const LocationEventEmitter: import("expo").EventEmitterType<Record<never, never>>;
//# sourceMappingURL=LocationEventEmitter.web.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"LocationEventEmitter.web.d.ts","sourceRoot":"","sources":["../src/LocationEventEmitter.web.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oBAAoB,uDAAqB,CAAC"}
@@ -0,0 +1,3 @@
import { EventEmitter } from 'expo-modules-core';
export const LocationEventEmitter = new EventEmitter();
//# sourceMappingURL=LocationEventEmitter.web.js.map
@@ -0,0 +1 @@
{"version":3,"file":"LocationEventEmitter.web.js","sourceRoot":"","sources":["../src/LocationEventEmitter.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,YAAY,EAAE,CAAC","sourcesContent":["import { EventEmitter } from 'expo-modules-core';\n\nexport const LocationEventEmitter = new EventEmitter();\n"]}
@@ -0,0 +1,37 @@
import { LocationCallback, LocationErrorCallback, LocationHeadingCallback } from './Location.types';
type EventObject = {
watchId: number;
[key: string]: any;
};
declare class Subscriber<CallbackType extends LocationCallback | LocationHeadingCallback | LocationErrorCallback> {
private eventName;
private eventDataField;
private callbacks;
private eventSubscription;
constructor(eventName: string, eventDataField: string);
maybeInitializeSubscription(): void;
/**
* Registers given callback under new id which is then returned.
*/
registerCallback(callback: CallbackType): number;
/**
* Registers given callback under and existing id. This can be used to
* create a subscriber for the error event on the same id as the location
* event is subscribed to.
*/
registerCallbackForId(watchId: number, callback: CallbackType): number;
/**
* Unregisters a callback with given id and revokes the subscription if possible.
*/
unregisterCallback(id: number): void;
trigger(event: EventObject): void;
}
export declare const LocationSubscriber: Subscriber<LocationCallback>;
export declare const HeadingSubscriber: Subscriber<LocationHeadingCallback>;
export declare const LocationErrorSubscriber: Subscriber<LocationErrorCallback>;
/**
* @private Necessary for some unit tests.
*/
export declare function _getCurrentWatchId(): number;
export {};
//# sourceMappingURL=LocationSubscribers.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"LocationSubscribers.d.ts","sourceRoot":"","sources":["../src/LocationSubscribers.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAGpG,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAC;AAIF,cAAM,UAAU,CACd,YAAY,SAAS,gBAAgB,GAAG,uBAAuB,GAAG,qBAAqB;IAEvF,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,iBAAiB,CAAkC;gBAE/C,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;IAKrD,2BAA2B;IAU3B;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM;IAOhD;;;;OAIG;IACH,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,MAAM;IAOtE;;OAEG;IACH,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAepC,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;CAUlC;AAED,eAAO,MAAM,kBAAkB,8BAG9B,CAAC;AACF,eAAO,MAAM,iBAAiB,qCAG7B,CAAC;AAEF,eAAO,MAAM,uBAAuB,mCAGnC,CAAC;AAEF;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
+74
View File
@@ -0,0 +1,74 @@
import ExpoLocation from './ExpoLocation';
import { LocationEventEmitter } from './LocationEventEmitter';
let nextWatchId = 0;
class Subscriber {
eventName;
eventDataField;
callbacks = {};
eventSubscription = null;
constructor(eventName, eventDataField) {
this.eventName = eventName;
this.eventDataField = eventDataField;
}
maybeInitializeSubscription() {
if (this.eventSubscription) {
return;
}
this.eventSubscription = LocationEventEmitter.addListener(this.eventName, (event) => this.trigger(event));
}
/**
* Registers given callback under new id which is then returned.
*/
registerCallback(callback) {
this.maybeInitializeSubscription();
const id = ++nextWatchId;
this.callbacks[id] = callback;
return id;
}
/**
* Registers given callback under and existing id. This can be used to
* create a subscriber for the error event on the same id as the location
* event is subscribed to.
*/
registerCallbackForId(watchId, callback) {
this.maybeInitializeSubscription();
const id = watchId;
this.callbacks[id] = callback;
return id;
}
/**
* Unregisters a callback with given id and revokes the subscription if possible.
*/
unregisterCallback(id) {
// Do nothing if we have already unregistered the callback.
if (!this.callbacks[id]) {
return;
}
delete this.callbacks[id];
ExpoLocation.removeWatchAsync(id);
if (Object.keys(this.callbacks).length === 0 && this.eventSubscription) {
LocationEventEmitter.removeSubscription(this.eventSubscription);
this.eventSubscription = null;
}
}
trigger(event) {
const watchId = event.watchId;
const callback = this.callbacks[watchId];
if (callback) {
callback(event[this.eventDataField]);
}
else {
ExpoLocation.removeWatchAsync(watchId);
}
}
}
export const LocationSubscriber = new Subscriber('Expo.locationChanged', 'location');
export const HeadingSubscriber = new Subscriber('Expo.headingChanged', 'heading');
export const LocationErrorSubscriber = new Subscriber('Expo.locationError', 'reason');
/**
* @private Necessary for some unit tests.
*/
export function _getCurrentWatchId() {
return nextWatchId;
}
//# sourceMappingURL=LocationSubscribers.js.map
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
export { PermissionStatus, type PermissionHookOptions, type PermissionExpiration, } from 'expo-modules-core';
export { LocationEventEmitter as EventEmitter } from './LocationEventEmitter';
export { _getCurrentWatchId } from './LocationSubscribers';
export { installWebGeolocationPolyfill } from './GeolocationPolyfill';
export * from './Location';
export * from './Location.types';
export { LocationAccuracy as Accuracy, LocationActivityType as ActivityType, LocationGeofencingEventType as GeofencingEventType, LocationGeofencingRegionState as GeofencingRegionState, } from './Location.types';
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,oBAAoB,IAAI,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,6BAA6B,EAAE,MAAM,uBAAuB,CAAC;AAEtE,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EACL,gBAAgB,IAAI,QAAQ,EAC5B,oBAAoB,IAAI,YAAY,EACpC,2BAA2B,IAAI,mBAAmB,EAClD,6BAA6B,IAAI,qBAAqB,GACvD,MAAM,kBAAkB,CAAC"}
+8
View File
@@ -0,0 +1,8 @@
export { PermissionStatus, } from 'expo-modules-core';
export { LocationEventEmitter as EventEmitter } from './LocationEventEmitter';
export { _getCurrentWatchId } from './LocationSubscribers';
export { installWebGeolocationPolyfill } from './GeolocationPolyfill';
export * from './Location';
export * from './Location.types';
export { LocationAccuracy as Accuracy, LocationActivityType as ActivityType, LocationGeofencingEventType as GeofencingEventType, LocationGeofencingRegionState as GeofencingRegionState, } from './Location.types';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,GAGjB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,oBAAoB,IAAI,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,6BAA6B,EAAE,MAAM,uBAAuB,CAAC;AAEtE,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EACL,gBAAgB,IAAI,QAAQ,EAC5B,oBAAoB,IAAI,YAAY,EACpC,2BAA2B,IAAI,mBAAmB,EAClD,6BAA6B,IAAI,qBAAqB,GACvD,MAAM,kBAAkB,CAAC","sourcesContent":["export {\n PermissionStatus,\n type PermissionHookOptions,\n type PermissionExpiration,\n} from 'expo-modules-core';\n\nexport { LocationEventEmitter as EventEmitter } from './LocationEventEmitter';\nexport { _getCurrentWatchId } from './LocationSubscribers';\nexport { installWebGeolocationPolyfill } from './GeolocationPolyfill';\n\nexport * from './Location';\nexport * from './Location.types';\n\nexport {\n LocationAccuracy as Accuracy,\n LocationActivityType as ActivityType,\n LocationGeofencingEventType as GeofencingEventType,\n LocationGeofencingRegionState as GeofencingRegionState,\n} from './Location.types';\n"]}
+15
View File
@@ -0,0 +1,15 @@
{
"platforms": ["apple", "android"],
"apple": {
"modules": ["LocationModule"]
},
"android": {
"modules": ["expo.modules.location.LocationModule"],
"publication": {
"groupId": "host.exp.exponent",
"artifactId": "expo.modules.location",
"version": "19.0.8",
"repository": "local-maven-repo"
}
}
}
@@ -0,0 +1,14 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import ExpoModulesCore
extension CLLocation: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
if let value = value as? [String: Any] {
let args = try Conversions.pickValues(from: value, byKeys: ["latitude", "longitude"], as: Double.self)
// swiftlint:disable:next force_cast
return CLLocation(latitude: args[0], longitude: args[1]) as! Self
}
throw Conversions.ConvertingException<CLLocation>(value)
}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocation.h>
#import <CoreLocation/CLLocationManager.h>
// Location accuracies
typedef NS_ENUM(NSUInteger, EXLocationAccuracy) {
EXLocationAccuracyLowest = 1,
EXLocationAccuracyLow = 2,
EXLocationAccuracyBalanced = 3,
EXLocationAccuracyHigh = 4,
EXLocationAccuracyHighest = 5,
EXLocationAccuracyBestForNavigation = 6,
};
@interface EXLocation : NSObject
+ (NSDictionary *)exportLocation:(CLLocation *)location;
+ (CLLocationAccuracy)CLLocationAccuracyFromOption:(EXLocationAccuracy)accuracy;
+ (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType;
@end
+58
View File
@@ -0,0 +1,58 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <ExpoLocation/EXLocation.h>
NS_ASSUME_NONNULL_BEGIN
@implementation EXLocation
+ (NSDictionary *)exportLocation:(CLLocation *)location
{
return @{
@"coords": @{
@"latitude": @(location.coordinate.latitude),
@"longitude": @(location.coordinate.longitude),
@"altitude": @(location.altitude),
@"accuracy": @(location.horizontalAccuracy),
@"altitudeAccuracy": @(location.verticalAccuracy),
@"heading": @(location.course),
@"speed": @(location.speed),
},
@"timestamp": @([location.timestamp timeIntervalSince1970] * 1000),
};
}
+ (CLLocationAccuracy)CLLocationAccuracyFromOption:(EXLocationAccuracy)accuracy
{
switch (accuracy) {
case EXLocationAccuracyLowest:
return kCLLocationAccuracyThreeKilometers;
case EXLocationAccuracyLow:
return kCLLocationAccuracyKilometer;
case EXLocationAccuracyBalanced:
return kCLLocationAccuracyHundredMeters;
case EXLocationAccuracyHigh:
return kCLLocationAccuracyNearestTenMeters;
case EXLocationAccuracyHighest:
return kCLLocationAccuracyBest;
case EXLocationAccuracyBestForNavigation:
return kCLLocationAccuracyBestForNavigation;
default:
return kCLLocationAccuracyHundredMeters;
}
}
+ (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType
{
if (activityType >= CLActivityTypeOther && activityType <= CLActivityTypeOtherNavigation) {
return activityType;
}
if (activityType == CLActivityTypeAirborne) {
return activityType;
}
return CLActivityTypeOther;
}
@end
NS_ASSUME_NONNULL_END
+27
View File
@@ -0,0 +1,27 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
Pod::Spec.new do |s|
s.name = 'ExpoLocation'
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platforms = {
:ios => '15.1'
}
s.source = { git: 'https://github.com/expo/expo.git' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
}
s.source_files = "**/*.{h,m,swift}"
end
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2024-present 650 Industries. All rights reserved.
import CoreLocation
internal struct Geocoder {
static func geocode(address: String) async throws -> [[String: Any?]] {
do {
let geocoder = CLGeocoder()
let placemarks = try await geocoder.geocodeAddressString(address)
return placemarks.map { placemark in
let location = placemark.location
return [
"latitude": location?.coordinate.latitude,
"longitude": location?.coordinate.longitude,
"altitude": location?.altitude,
"accuracy": location?.horizontalAccuracy
]
}
} catch {
return try handleCLError(error: error as NSError, defaultValue: [])
}
}
static func reverseGeocode(location: CLLocation) async throws -> [[String: Any?]] {
do {
let geocoder = CLGeocoder()
let placemarks = try await geocoder.reverseGeocodeLocation(location)
return placemarks.map { placemark in
return [
"city": placemark.locality,
"district": placemark.subLocality,
"streetNumber": placemark.subThoroughfare,
"street": placemark.thoroughfare,
"region": placemark.administrativeArea,
"subregion": placemark.subAdministrativeArea,
"country": placemark.country,
"postalCode": placemark.postalCode,
"name": placemark.name,
"isoCountryCode": placemark.isoCountryCode,
"timezone": placemark.timeZone?.identifier
]
}
} catch {
return try handleCLError(error: error as NSError, defaultValue: [])
}
}
}
@@ -0,0 +1,8 @@
// Copyright 2024-present 650 Industries. All rights reserved.
import ExpoModulesCore
internal struct LastKnownLocationRequirements: Record {
@Field var maxAge: Double = .greatestFiniteMagnitude
@Field var requiredAccuracy: Double = .greatestFiniteMagnitude
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import ExpoModulesCore
internal enum LocationAccuracy: Int, Enumerable {
case lowest = 1
case low = 2
case balanced = 3
case high = 4
case highest = 5
case bestForNavigation = 6
func toCLLocationAccuracy() -> CLLocationAccuracy {
switch self {
case .lowest:
return kCLLocationAccuracyThreeKilometers
case .low:
return kCLLocationAccuracyKilometer
case .balanced:
return kCLLocationAccuracyHundredMeters
case .high:
return kCLLocationAccuracyNearestTenMeters
case .highest:
return kCLLocationAccuracyBest
case .bestForNavigation:
return kCLLocationAccuracyBestForNavigation
}
}
}
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2024-present 650 Industries. All rights reserved.
import ExpoModulesCore
extension Exceptions {
internal final class LocationUnavailable: Exception {
override var reason: String {
"Cannot obtain current location"
}
}
internal final class LocationRequestCanceled: Exception {
override var reason: String {
"Requesting the location has been canceled"
}
}
internal final class GeocodingNetwork: Exception {
override var reason: String {
"Geocoding rate limit exceeded - too many requests"
}
}
internal final class GeocodingFailed: Exception {
override var reason: String {
"Error while geocoding a location"
}
}
internal final class TaskManagerUnavailable: Exception {
override var reason: String {
"'expo-task-manager' module is required to use background services"
}
}
internal final class LocationUpdatesUnavailable: Exception {
override var reason: String {
"Background location has not been configured, make sure to add 'location' to 'UIBackgroundModes' in the Info.plist file"
}
}
internal final class HeadingUnavailableException: Exception {
override var reason: String {
"Heading updates not available"
}
}
internal final class GeofencingUnavailable: Exception {
override var reason: String {
"Geofencing is not available"
}
}
internal final class LocationServicesDisabled: Exception {
override var reason: String {
"Location services are disabled"
}
}
internal final class DeniedForegroundLocationPermission: Exception {
override var reason: String {
"Location permission is required to do this operation"
}
}
internal final class DeniedBackgroundLocationPermission: Exception {
override var reason: String {
"Background location permission is required to do this operation"
}
}
}
+223
View File
@@ -0,0 +1,223 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import CoreLocation
import ExpoModulesCore
private let EVENT_LOCATION_CHANGED = "Expo.locationChanged"
private let EVENT_HEADING_CHANGED = "Expo.headingChanged"
private let EVENT_LOCATION_ERROR = "Expo.locationError"
public final class LocationModule: Module {
private lazy var locationStreamers = [Int: BaseStreamer]()
private var taskManager: EXTaskManagerInterface {
get throws {
guard let taskManager: EXTaskManagerInterface = appContext?.legacyModule(implementing: EXTaskManagerInterface.self) else {
throw Exceptions.TaskManagerUnavailable()
}
return taskManager
}
}
public func definition() -> ModuleDefinition {
Name("ExpoLocation")
Events(EVENT_LOCATION_CHANGED, EVENT_HEADING_CHANGED, EVENT_LOCATION_ERROR)
OnCreate {
let permissionsManager = self.appContext?.permissions
EXPermissionsMethodsDelegate.register(
[
EXLocationPermissionRequester(),
EXForegroundPermissionRequester(),
EXBackgroundLocationPermissionRequester()
],
withPermissionsManager: permissionsManager
)
}
AsyncFunction("getProviderStatusAsync") {
return [
"locationServicesEnabled": CLLocationManager.locationServicesEnabled(),
"backgroundModeEnabled": true
]
}
AsyncFunction("getCurrentPositionAsync") { (options: LocationOptions) -> [String: Any] in
try ensureForegroundLocationPermissions(appContext)
let requester = await LocationRequester(options: options)
let location = try await requester.requestLocation()
return exportLocation(location)
}
AsyncFunction("watchPositionImplAsync") { (watchId: Int, options: LocationOptions) in
try ensureForegroundLocationPermissions(appContext)
let streamer = await LocationsStreamer(options: options)
locationStreamers[watchId] = streamer
// Start streaming in another task, so the returned promise is not waiting for the stream to end.
Task {
do {
for try await locations in try streamer.streamLocations() {
guard let location = locations.last else {
continue
}
sendEvent(EVENT_LOCATION_CHANGED, [
"watchId": watchId,
"location": exportLocation(location)
])
}
} catch let exception as Exception {
sendEvent(EVENT_LOCATION_ERROR, ["watchId": watchId, "reason": exception.reason])
} catch {
sendEvent(EVENT_LOCATION_ERROR, ["watchId": watchId, "reason": error.localizedDescription])
}
}
}
AsyncFunction("getLastKnownPositionAsync") { (requirements: LastKnownLocationRequirements) -> [String: Any]? in
try ensureForegroundLocationPermissions(appContext)
if let location = CLLocationManager().location, isLocation(location, valid: requirements) {
return exportLocation(location)
}
return nil
}
AsyncFunction("watchDeviceHeading") { (watchId: Int) in
try ensureForegroundLocationPermissions(appContext)
let options = LocationOptions(accuracy: .bestForNavigation, distanceInterval: 0)
let streamer = await DeviceHeadingStreamer(options: options)
locationStreamers[watchId] = streamer
// Start streaming in another task, so the returned promise is not waiting for the stream to end.
Task {
do {
for try await heading in try streamer.streamDeviceHeading() {
sendEvent(EVENT_HEADING_CHANGED, [
"watchId": watchId,
"heading": [
"trueHeading": heading.trueHeading,
"magHeading": heading.magneticHeading,
"accuracy": normalizeAccuracy(heading.headingAccuracy)
]
])
}
} catch let exception as Exception {
sendEvent(EVENT_LOCATION_ERROR, ["watchId": watchId, "reason": exception.reason])
} catch {
sendEvent(EVENT_LOCATION_ERROR, ["watchId": watchId, "reason": error.localizedDescription])
}
}
}
AsyncFunction("removeWatchAsync") { (watchId: Int) in
if let streamer = locationStreamers[watchId] {
streamer.stopStreaming()
}
locationStreamers[watchId] = nil
}
AsyncFunction("geocodeAsync") { (address: String) in
return try await Geocoder.geocode(address: address)
}
AsyncFunction("reverseGeocodeAsync") { (location: CLLocation) in
return try await Geocoder.reverseGeocode(location: location)
}
AsyncFunction("getPermissionsAsync") { (promise: Promise) in
try getPermissionUsingRequester(EXLocationPermissionRequester.self, appContext: appContext, promise: promise)
}
AsyncFunction("requestPermissionsAsync") { (promise: Promise) in
try askForPermissionUsingRequester(EXLocationPermissionRequester.self, appContext: appContext, promise: promise)
}
AsyncFunction("getForegroundPermissionsAsync") { (promise: Promise) in
try getPermissionUsingRequester(EXForegroundPermissionRequester.self, appContext: appContext, promise: promise)
}
AsyncFunction("requestForegroundPermissionsAsync") { (promise: Promise) in
try askForPermissionUsingRequester(EXForegroundPermissionRequester.self, appContext: appContext, promise: promise)
}
AsyncFunction("getBackgroundPermissionsAsync") { (promise: Promise) in
try getPermissionUsingRequester(EXBackgroundLocationPermissionRequester.self, appContext: appContext, promise: promise)
}
AsyncFunction("requestBackgroundPermissionsAsync") { (promise: Promise) in
try askForPermissionUsingRequester(EXBackgroundLocationPermissionRequester.self, appContext: appContext, promise: promise)
}
AsyncFunction("hasServicesEnabledAsync") {
return CLLocationManager.locationServicesEnabled()
}
// Background location
AsyncFunction("startLocationUpdatesAsync") { (taskName: String, options: [String: Any]) in
// There are two ways of starting this service.
// 1. As a background location service, this requires the background location permission.
// 2. As a user-initiated foreground service, this does NOT require the background location permission.
// Unfortunately, we cannot distinguish between those cases.
// So we only check foreground permission which needs to be granted in both cases.
try ensureLocationServicesEnabled()
try ensureForegroundLocationPermissions(appContext)
guard CLLocationManager.significantLocationChangeMonitoringAvailable() else {
throw Exceptions.LocationUpdatesUnavailable()
}
guard try taskManager.hasBackgroundModeEnabled("location") else {
throw Exceptions.LocationUpdatesUnavailable()
}
try taskManager.registerTask(withName: taskName, consumer: EXLocationTaskConsumer.self, options: options)
}
AsyncFunction("stopLocationUpdatesAsync") { (taskName: String) in
let taskManager = try taskManager
try EXUtilities.catchException {
taskManager.unregisterTask(withName: taskName, consumerClass: EXLocationTaskConsumer.self)
}
}
AsyncFunction("hasStartedLocationUpdatesAsync") { (taskName: String) -> Bool in
return try taskManager.task(withName: taskName, hasConsumerOf: EXLocationTaskConsumer.self)
}
// Geofencing
AsyncFunction("startGeofencingAsync") { (taskName: String, options: [String: Any]) in
try ensureBackgroundLocationPermissions(appContext)
guard CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) else {
throw Exceptions.GeofencingUnavailable()
}
guard try taskManager.hasBackgroundModeEnabled("location") else {
throw Exceptions.LocationUpdatesUnavailable()
}
try taskManager.registerTask(withName: taskName, consumer: EXGeofencingTaskConsumer.self, options: options)
}
AsyncFunction("stopGeofencingAsync") { (taskName: String) in
let taskManager = try taskManager
try EXUtilities.catchException {
taskManager.unregisterTask(withName: taskName, consumerClass: EXGeofencingTaskConsumer.self)
}
}
AsyncFunction("hasStartedGeofencingAsync") { (taskName: String) -> Bool in
return try taskManager.task(withName: taskName, hasConsumerOf: EXGeofencingTaskConsumer.self)
}
}
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import ExpoModulesCore
internal struct LocationOptions: Record {
@Field var accuracy: LocationAccuracy = .balanced
@Field var distanceInterval: Double = 0.0
}
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2023-present 650 Industries. All rights reserved.
import ExpoModulesCore
/**
Converts iOS heading accuracy to Android system.
3: high accuracy, 2: medium, 1: low, 0: none
*/
internal func normalizeAccuracy(_ accuracy: CLLocationDirection) -> Int {
if accuracy > 50 || accuracy < 0 {
return 0
}
if accuracy > 35 {
return 1
}
if accuracy > 20 {
return 2
}
return 3
}
internal func exportLocation(_ location: CLLocation) -> [String: Any] {
return [
"coords": [
"latitude": location.coordinate.latitude,
"longitude": location.coordinate.longitude,
"altitude": location.altitude,
"accuracy": location.horizontalAccuracy,
"altitudeAccuracy": location.verticalAccuracy,
"heading": location.course,
"speed": location.speed
],
"timestamp": location.timestamp.timeIntervalSince1970 * 1000
]
}
internal func handleCLError<ReturnType>(error: NSError, defaultValue: ReturnType) throws -> ReturnType {
switch CLError.Code(rawValue: error.code) {
case CLError.geocodeFoundNoResult, CLError.geocodeFoundPartialResult:
return defaultValue
case CLError.network:
throw Exceptions.GeocodingNetwork()
default:
throw Exceptions.GeocodingFailed().causedBy(error)
}
}
internal func ensureLocationServicesEnabled() throws {
guard CLLocationManager.locationServicesEnabled() else {
throw Exceptions.LocationServicesDisabled()
}
}
// MARK: - Permissions
internal func getPermissionUsingRequester<Requester: EXPermissionsRequester>(
_ requester: Requester.Type,
appContext: AppContext?,
promise: Promise
) throws {
guard let permissionsManager = appContext?.permissions else {
throw Exceptions.PermissionsModuleNotFound()
}
EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager(
permissionsManager,
withRequester: Requester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
internal func askForPermissionUsingRequester<Requester: EXPermissionsRequester>(
_ requester: Requester.Type,
appContext: AppContext?,
promise: Promise
) throws {
guard let permissionsManager = appContext?.permissions else {
throw Exceptions.PermissionsModuleNotFound()
}
EXPermissionsMethodsDelegate.askForPermission(
withPermissionsManager: permissionsManager,
withRequester: Requester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
internal func checkPermissionWithRequester<Requester: EXPermissionsRequester>(
_ requester: Requester.Type,
appContext: AppContext?
) throws -> Bool {
guard let permissionsManager = appContext?.permissions else {
throw Exceptions.PermissionsModuleNotFound()
}
return permissionsManager.hasGrantedPermission(usingRequesterClass: Requester.self)
}
func ensureForegroundLocationPermissions(_ appContext: AppContext?) throws {
guard try checkPermissionWithRequester(EXForegroundPermissionRequester.self, appContext: appContext) else {
throw Exceptions.DeniedForegroundLocationPermission()
}
}
func ensureBackgroundLocationPermissions(_ appContext: AppContext?) throws {
guard try checkPermissionWithRequester(EXBackgroundLocationPermissionRequester.self, appContext: appContext) else {
throw Exceptions.DeniedBackgroundLocationPermission()
}
}
// MARK: - Other utils
func isLocation(_ location: CLLocation?, valid requirements: LastKnownLocationRequirements) -> Bool {
guard let location else {
return false
}
let timeDiff = -location.timestamp.timeIntervalSinceNow
return timeDiff * 1000 <= requirements.maxAge && location.horizontalAccuracy <= requirements.requiredAccuracy
}
@@ -0,0 +1,17 @@
// Copyright 2024-present 650 Industries. All rights reserved.
import CoreLocation
internal class BaseLocationProvider: NSObject, CLLocationManagerDelegate {
internal let manager = CLLocationManager()
// CLLocationManager must be created on the main thread.
@MainActor
init(options: LocationOptions) {
super.init()
manager.allowsBackgroundLocationUpdates = false
manager.distanceFilter = options.distanceInterval
manager.desiredAccuracy = options.accuracy.toCLLocationAccuracy()
manager.delegate = self
}
}
@@ -0,0 +1,10 @@
// Copyright 2024-present 650 Industries. All rights reserved.
import CoreLocation
import ExpoModulesCore
internal class BaseStreamer: BaseLocationProvider {
func stopStreaming() {
// Default empty implementation
}
}
@@ -0,0 +1,53 @@
// Copyright 2024-present 650 Industries. All rights reserved.
import CoreLocation
import ExpoModulesCore
internal class DeviceHeadingStreamer: BaseStreamer {
typealias DeviceHeadingStream = AsyncThrowingStream<CLHeading, Error>
private var headingStream: DeviceHeadingStream?
private var continuation: DeviceHeadingStream.Continuation?
deinit {
if continuation != nil {
stopStreaming()
}
}
func streamDeviceHeading() throws -> DeviceHeadingStream {
if !CLLocationManager.headingAvailable() {
// Throw error
throw Exceptions.HeadingUnavailableException()
}
if let stream = headingStream {
return stream
}
let stream = DeviceHeadingStream { continuation in
self.continuation = continuation
manager.startUpdatingHeading()
}
headingStream = stream
return stream
}
override func stopStreaming() {
manager.stopUpdatingHeading()
continuation?.finish()
headingStream = nil
continuation = nil
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
continuation?.yield(newHeading)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
continuation?.finish(throwing: Exceptions.HeadingUnavailableException().causedBy(error))
headingStream = nil
continuation = nil
}
}
@@ -0,0 +1,36 @@
// Copyright 2024-present 650 Industries. All rights reserved.
import CoreLocation
import ExpoModulesCore
internal class LocationRequester: BaseLocationProvider {
private var continuation: CheckedContinuation<CLLocation, Error>?
deinit {
continuation?.resume(throwing: Exceptions.LocationRequestCanceled())
continuation = nil
}
func requestLocation() async throws -> CLLocation {
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
manager.requestLocation()
}
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let lastLocation = locations.last {
continuation?.resume(returning: lastLocation)
} else {
continuation?.resume(throwing: Exceptions.LocationUnavailable())
}
continuation = nil
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
continuation?.resume(throwing: Exceptions.LocationUnavailable().causedBy(error))
continuation = nil
}
}
@@ -0,0 +1,55 @@
// Copyright 2024-present 650 Industries. All rights reserved.
import CoreLocation
import ExpoModulesCore
internal class LocationsStreamer: BaseStreamer {
typealias LocationsStream = AsyncThrowingStream<[CLLocation], Error>
private var locationsStream: LocationsStream?
private var continuation: LocationsStream.Continuation?
deinit {
if continuation != nil {
stopStreaming()
}
}
func streamLocations() throws -> LocationsStream {
if !CLLocationManager.locationServicesEnabled() {
throw Exceptions.LocationServicesDisabled()
}
if let stream = locationsStream {
return stream
}
let stream = LocationsStream { continuation in
self.continuation = continuation
manager.startUpdatingLocation()
}
locationsStream = stream
return stream
}
override func stopStreaming() {
manager.stopUpdatingLocation()
continuation?.finish()
locationsStream = nil
continuation = nil
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if !locations.isEmpty {
continuation?.yield(locations)
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
continuation?.finish(throwing: Exceptions.LocationUnavailable().causedBy(error))
locationsStream = nil
continuation = nil
}
}
@@ -0,0 +1,7 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <ExpoLocation/EXBaseLocationRequester.h>
@interface EXBackgroundLocationPermissionRequester : EXBaseLocationRequester
@end
@@ -0,0 +1,157 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <ExpoLocation/EXBackgroundLocationPermissionRequester.h>
#import <objc/message.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
static SEL alwaysAuthorizationSelector;
@interface EXBackgroundLocationPermissionRequester ()
@property (nonatomic, assign) bool wasAsked;
@property (nonatomic, assign) bool isWaitingForTimeout;
@end
@implementation EXBackgroundLocationPermissionRequester
- (instancetype)init
{
if (self = [super init]) {
_wasAsked = false;
_isWaitingForTimeout = false;
}
return self;
}
+ (NSString *)permissionType
{
return @"locationBackground";
}
+ (void)load
{
alwaysAuthorizationSelector = NSSelectorFromString([@"request" stringByAppendingString:@"AlwaysAuthorization"]);
}
- (void)requestLocationPermissions
{
if ([EXBaseLocationRequester isConfiguredForAlwaysAuthorization] && [self.locationManager respondsToSelector:alwaysAuthorizationSelector]) {
_wasAsked = true;
CLAuthorizationStatus status = [self.locationManager authorizationStatus];
if (status == kCLAuthorizationStatusAuthorizedWhenInUse) {
// We already have a foreground permission granted:
// When asking for background location, we might or might not have asked for foreground permission
// before we get here. An issue here is if the user has a temporary permission ("Allow once") - which
// results in the status being "kCLAuthorizationStatusAuthorizedWhenInUse" - without us knowing.
// We need to handle this special case which is not possible to detect through the API.
// What we do is that we'll wait 1.5 seconds on an UIApplicationWillResignActiveNotification
// notification (which will be emitted almost directly if the permission dialog is displayed). If the permission
// dialog is not displayed we'll timeout and can resolve the waiting promise with an updated denied status.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleAppBecomingInactive)
name:UIApplicationWillResignActiveNotification
object:nil];
// Setup timeout - if no permission dialog was displayed we can just stop listening and deny
// the request
[self setupAppInactivateTimeout];
}
// Request permissions
((void (*)(id, SEL))objc_msgSend)(self.locationManager, alwaysAuthorizationSelector);
} else {
self.reject(@"ERR_LOCATION_INFO_PLIST", @"One of the `NSLocation*UsageDescription` keys must be present in Info.plist to be able to use geolocation.", nil);
self.resolve = nil;
self.reject = nil;
}
}
- (void)handleAppBecomingActive
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (self.resolve) {
self.resolve([self getPermissions]);
self.resolve = nil;
self.reject = nil;
}
}
- (void)handleAppBecomingInactive
{
// Let's wait until the app becomes inactive - this happens when OS displays the
// permission dialog - then we can cancel the timeout handler.
_isWaitingForTimeout = false;
[[NSNotificationCenter defaultCenter] removeObserver:self];
// When the app is inactive it means that a permission dialog is showing and we should ask to be
// notified when the dialog is closed:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleAppBecomingActive)
name:UIApplicationDidBecomeActiveNotification
object:nil];
}
- (void)setupAppInactivateTimeout
{
_isWaitingForTimeout = true;
// Obtain a reference to the current queue
dispatch_queue_t currentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
// Calculate the time for the delay
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC));
EX_WEAKIFY(self);
// Schedule the block to be executed after the delay
dispatch_after(delayTime, currentQueue, ^{
EX_ENSURE_STRONGIFY(self)
// Check if we are still waiting - ie. we haven't seen a permission dialog
if (self.isWaitingForTimeout && self.resolve) {
self.isWaitingForTimeout = false;
self.resolve([self getPermissions]);
self.resolve = nil;
self.reject = nil;
}
});
}
- (NSDictionary *)parsePermissions:(CLAuthorizationStatus)systemStatus
{
EXPermissionStatus status;
switch (systemStatus) {
case kCLAuthorizationStatusAuthorizedAlways: {
status = EXPermissionStatusGranted;
break;
}
case kCLAuthorizationStatusDenied:
case kCLAuthorizationStatusRestricted: {
status = EXPermissionStatusDenied;
break;
}
case kCLAuthorizationStatusAuthorizedWhenInUse: {
if (_wasAsked) {
status = EXPermissionStatusDenied;
} else {
status = EXPermissionStatusUndetermined;
}
break;
}
case kCLAuthorizationStatusNotDetermined:
default: {
status = EXPermissionStatusUndetermined;
break;
}
}
return @{ @"status": @(status), @"scope": @(systemStatus == kCLAuthorizationStatusAuthorizedWhenInUse ? "whenInUse" : systemStatus == kCLAuthorizationStatusAuthorizedAlways ? "always" : "none") };
}
@end
@@ -0,0 +1,20 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocationManager.h>
#import <ExpoModulesCore/EXPermissionsInterface.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
@interface EXBaseLocationRequester : NSObject<EXPermissionsRequester, CLLocationManagerDelegate>
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) EXPromiseResolveBlock resolve;
@property (nonatomic, strong) EXPromiseRejectBlock reject;
+ (BOOL)isConfiguredForWhenInUseAuthorization;
+ (BOOL)isConfiguredForAlwaysAuthorization;
- (void)requestLocationPermissions;
- (NSDictionary *)parsePermissions:(CLAuthorizationStatus)systemStatus;
@end
@@ -0,0 +1,162 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <ExpoLocation/EXBaseLocationRequester.h>
#import <ExpoModulesCore/EXUtilities.h>
#import <objc/message.h>
@interface EXBaseLocationRequester () <CLLocationManagerDelegate>
@property (nonatomic, assign) bool locationManagerWasCalled;
@property (nonatomic, assign) CLAuthorizationStatus beginStatus;
@end
@implementation EXBaseLocationRequester
# pragma mark - Abstract methods
- (void)requestLocationPermissions
{
@throw([NSException exceptionWithName:@"NotImplemented" reason:@"requestLocationPermissions should be implemented" userInfo:nil]);
}
+ (NSString *)permissionType {
@throw([NSException exceptionWithName:@"NotImplemented" reason:@"permissionType should be implemented" userInfo:nil]);
}
- (NSDictionary *)parsePermissions:(CLAuthorizationStatus)systemStatus
{
@throw([NSException exceptionWithName:@"NotImplemented" reason:@"parsePermissions should be implemented" userInfo:nil]);
}
# pragma mark - UMPermissionsRequester
- (NSDictionary *)getPermissions {
CLAuthorizationStatus systemStatus;
if (![EXBaseLocationRequester isConfiguredForAlwaysAuthorization] && ![EXBaseLocationRequester isConfiguredForWhenInUseAuthorization]) {
EXFatal(EXErrorWithMessage(@"This app is missing usage descriptions, so location services will fail. Add one of the `NSLocation*UsageDescription` keys to your bundle's Info.plist. See https://bit.ly/3iLqy6S (https://docs.expo.dev/distribution/app-stores/#system-permissions-dialogs-on-ios) for more information."));
systemStatus = kCLAuthorizationStatusDenied;
} else {
systemStatus = [CLLocationManager authorizationStatus];
}
return [self parsePermissions:systemStatus];
}
- (void)requestPermissionsWithResolver:(EXPromiseResolveBlock)resolve rejecter:(EXPromiseRejectBlock)reject {
NSDictionary *existingPermissions = [self getPermissions];
if (existingPermissions && [existingPermissions[@"status"] intValue] != EXPermissionStatusUndetermined) {
// since permissions are already determined, the iOS request methods will be no-ops.
// just resolve with whatever existing permissions.
resolve(existingPermissions);
} else {
_resolve = resolve;
_reject = reject;
EX_WEAKIFY(self)
[EXUtilities performSynchronouslyOnMainThread:^{
EX_ENSURE_STRONGIFY(self)
self.locationManagerWasCalled = false;
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
}];
// 1. Why do we call CLLocationManager methods by those dynamically created selectors?
//
// Most probably application code submitted to Apple Store is statically analyzed
// paying special attention to camelcase(request_always_location) being called on CLLocationManager.
// This lets Apple warn developers when it notices that location authorization may be requested
// while there is no NSLocationUsageDescription in Info.plist. Since we want to neither
// make Expo developers receive this kind of messages nor add our own default usage description,
// we try to fool the static analyzer and construct the selector in runtime.
// This way behavior of this requester is governed by provided NSLocationUsageDescriptions.
// 2. Location permission request types
//
// Foreground
// - "Allow once"
// - "Allow while using App"
// - "Don't allow"
//
// Background
// - "Keep only while using"
// - "Change to always allow"
//
// Requesting background permissions directly without first asking for foreground permissions is the
// same as asking for foreground permissions and then asking for background permissions.
//
// "Allow once" is a temporary permission (limited to the current app session). It is not possible to get
// info from the API about wether or not the current permission is temporary. You cannot request background
// permissions with a temporary token - a background request will then return denied.
//
// Requesting background permissions directly and "Allow while using the App" gives you a provisional
// background permission that can later be elevated to a full "Always allow" permission.
// You will be asked at a later point if you want to convert to "Always allow". The system waits until
// you have started using the newly aquired permission before showing the permission dialog.
//
// Test the following scenarios in BareExpo -> APIs -> Location
// ------------------------------------------------------------
// (before tests, make sure to clear any location permissions and restart the app)
//
// rfp = requestForegroundPermissionsAsync, fp: Actual foreground permission given
// rbp = requestBackgroundPermissionsAsync, bg: Actual background permission given
//
// - rfp -> "Allow once", then rbp -> no dialog = (fp: granted (temporary), bg: denied after 1.5 seconds)
// - rfp -> "Allow while using App", then rbp -> "Keep only while using" = (fp: granted, bg: denied)
// - rfp -> "Allow while using App", then rbp -> "Change to always allow" = (fp: granted, bg: granted)
// - rfp -> "Don't allow", then rbp -> no dialog = (fp: denied, bg: denied)
// - rbp -> "Allow once", no more dalogs = (fp: granted (temporary), bg: denied)
// - rbp -> "Allow while using App", no more dialogs = (fp: granted, bg: granted (provisional))
// - rbp -> "Don't allow" = (fp: denied, bg: denied)
// Save start statue and call requestLocationPermissions
_beginStatus = [self.locationManager authorizationStatus];
[self requestLocationPermissions];
}
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
if (_reject) {
_reject(@"E_LOCATION_ERROR_UNKNOWN", error.localizedDescription, error);
_resolve = nil;
_reject = nil;
}
}
- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager
{
CLAuthorizationStatus nextState = [manager authorizationStatus];
if (_beginStatus == nextState && !_locationManagerWasCalled) {
// CLLocationManager calls this delegate method once on start with kCLAuthorizationNotDetermined even before the user responds
// to the "Don't Allow" / "Allow" dialog box. This isn't the event we care about so we skip it. See:
// http://stackoverflow.com/questions/30106341/swift-locationmanager-didchangeauthorizationstatus-always-called/30107511#30107511
_locationManagerWasCalled = true;
return;
}
if (_resolve) {
_resolve([self getPermissions]);
_resolve = nil;
_reject = nil;
}
}
#pragma mark - Helpers
+ (BOOL)isConfiguredForWhenInUseAuthorization
{
return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"] != nil;
}
+ (BOOL)isConfiguredForAlwaysAuthorization
{
return [self isConfiguredForWhenInUseAuthorization] && [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysAndWhenInUseUsageDescription"];
}
@end
@@ -0,0 +1,8 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <ExpoLocation/EXBaseLocationRequester.h>
@interface EXForegroundPermissionRequester : EXBaseLocationRequester
@end
@@ -0,0 +1,60 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <ExpoLocation/EXForegroundPermissionRequester.h>
#import <ExpoModulesCore/EXUtilities.h>
#import <objc/message.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
static SEL whenInUseAuthorizationSelector;
@implementation EXForegroundPermissionRequester
+ (NSString *)permissionType
{
return @"locationForeground";
}
+ (void)load
{
whenInUseAuthorizationSelector = NSSelectorFromString([@"request" stringByAppendingString:@"WhenInUseAuthorization"]);
}
- (void)requestLocationPermissions
{
if ([EXBaseLocationRequester isConfiguredForWhenInUseAuthorization] && [self.locationManager respondsToSelector:whenInUseAuthorizationSelector]) {
((void (*)(id, SEL))objc_msgSend)(self.locationManager, whenInUseAuthorizationSelector);
} else {
self.reject(@"ERR_LOCATION_INFO_PLIST", @"The `NSLocationWhenInUseUsageDescription` key must be present in Info.plist to be able to use geolocation.", nil);
self.resolve = nil;
self.reject = nil;
}
}
- (NSDictionary *)parsePermissions:(CLAuthorizationStatus)systemStatus
{
EXPermissionStatus status;
switch (systemStatus) {
case kCLAuthorizationStatusAuthorizedWhenInUse:
case kCLAuthorizationStatusAuthorizedAlways: {
status = EXPermissionStatusGranted;
break;
}
case kCLAuthorizationStatusDenied:
case kCLAuthorizationStatusRestricted: {
status = EXPermissionStatusDenied;
break;
}
case kCLAuthorizationStatusNotDetermined:
default: {
status = EXPermissionStatusUndetermined;
break;
}
}
return @{ @"status": @(status), @"scope": @(systemStatus == kCLAuthorizationStatusAuthorizedWhenInUse ? "whenInUse" : systemStatus == kCLAuthorizationStatusAuthorizedAlways ? "always" : "none") };
}
@end
@@ -0,0 +1,7 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <ExpoLocation/EXBaseLocationRequester.h>
@interface EXLocationPermissionRequester : EXBaseLocationRequester
@end
@@ -0,0 +1,72 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <ExpoLocation/EXLocationPermissionRequester.h>
#import <objc/message.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
static SEL alwaysAuthorizationSelector;
static SEL whenInUseAuthorizationSelector;
@implementation EXLocationPermissionRequester
+ (NSString *)permissionType
{
return @"location";
}
+ (void)load
{
alwaysAuthorizationSelector = NSSelectorFromString([@"request" stringByAppendingString:@"AlwaysAuthorization"]);
whenInUseAuthorizationSelector = NSSelectorFromString([@"request" stringByAppendingString:@"WhenInUseAuthorization"]);
}
- (void)requestLocationPermissions
{
if ([EXBaseLocationRequester isConfiguredForAlwaysAuthorization] && [self.locationManager respondsToSelector:alwaysAuthorizationSelector]) {
((void (*)(id, SEL))objc_msgSend)(self.locationManager, alwaysAuthorizationSelector);
} else if ([EXBaseLocationRequester isConfiguredForWhenInUseAuthorization] && [self.locationManager respondsToSelector:whenInUseAuthorizationSelector]) {
((void (*)(id, SEL))objc_msgSend)(self.locationManager, whenInUseAuthorizationSelector);
} else {
self.reject(@"E_LOCATION_INFO_PLIST", @"One of the `NSLocation*UsageDescription` keys must be present in Info.plist to be able to use geolocation.", nil);
self.resolve = nil;
self.reject = nil;
}
}
- (NSDictionary *)parsePermissions:(CLAuthorizationStatus)systemStatus
{
EXPermissionStatus status;
NSString *scope = @"none";
switch (systemStatus) {
case kCLAuthorizationStatusAuthorizedWhenInUse: {
status = EXPermissionStatusGranted;
scope = @"whenInUse";
break;
}
case kCLAuthorizationStatusAuthorizedAlways: {
status = EXPermissionStatusGranted;
scope = @"always";
break;
}
case kCLAuthorizationStatusDenied:
case kCLAuthorizationStatusRestricted: {
status = EXPermissionStatusDenied;
break;
}
case kCLAuthorizationStatusNotDetermined:
default: {
status = EXPermissionStatusUndetermined;
break;
}
}
return @{
@"status": @(status),
@"scope": scope
};
}
@end
@@ -0,0 +1,27 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocationManagerDelegate.h>
#import <ExpoModulesCore/EXTaskConsumerInterface.h>
NS_ASSUME_NONNULL_BEGIN
// Geofencing event types
typedef NS_ENUM(NSUInteger, EXGeofencingEventType) {
EXGeofencingEventTypeEnter = 1,
EXGeofencingEventTypeExit = 2,
};
// Geofencing region states
typedef NS_ENUM(NSUInteger, EXGeofencingRegionState) {
EXGeofencingRegionStateUnknown = 0,
EXGeofencingRegionStateInside = 1,
EXGeofencingRegionStateOutside = 2,
};
@interface EXGeofencingTaskConsumer : NSObject <EXTaskConsumerInterface, CLLocationManagerDelegate>
@property (nonatomic, strong) id<EXTaskInterface> task;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,215 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <CoreLocation/CLCircularRegion.h>
#import <CoreLocation/CLLocationManager.h>
#import <CoreLocation/CLErrorDomain.h>
#import <ExpoModulesCore/EXUtilities.h>
#import <ExpoModulesCore/EXTaskInterface.h>
#import <ExpoLocation/EXLocation.h>
#import <ExpoLocation/EXGeofencingTaskConsumer.h>
@interface EXGeofencingTaskConsumer ()
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *regionStates;
@property (nonatomic, assign) BOOL backgroundOnly;
@end
@implementation EXGeofencingTaskConsumer
- (void)dealloc
{
[self reset];
}
# pragma mark - EXTaskConsumerInterface
- (NSString *)taskType
{
return @"geofencing";
}
- (void)setOptions:(nonnull NSDictionary *)options
{
[self stopMonitoringAllRegions];
[self startMonitoringRegionsForTask:self->_task];
}
- (void)didRegisterTask:(id<EXTaskInterface>)task
{
[self startMonitoringRegionsForTask:task];
}
- (void)didUnregister
{
[self reset];
}
# pragma mark - helpers
- (void)reset
{
[self stopMonitoringAllRegions];
[EXUtilities performSynchronouslyOnMainThread:^{
self->_locationManager = nil;
self->_task = nil;
}];
}
- (void)startMonitoringRegionsForTask:(id<EXTaskInterface>)task
{
[EXUtilities performSynchronouslyOnMainThread:^{
CLLocationManager *locationManager = [CLLocationManager new];
NSMutableDictionary *regionStates = [NSMutableDictionary new];
NSDictionary *options = [task options];
NSArray *regions = options[@"regions"];
self->_task = task;
self->_locationManager = locationManager;
self->_regionStates = regionStates;
locationManager.delegate = self;
locationManager.allowsBackgroundLocationUpdates = YES;
locationManager.pausesLocationUpdatesAutomatically = NO;
for (NSDictionary *regionDict in regions) {
NSString *identifier = regionDict[@"identifier"] ?: [[NSUUID UUID] UUIDString];
CLLocationDistance radius = [regionDict[@"radius"] doubleValue];
CLLocationCoordinate2D center = [self.class coordinateFromDictionary:regionDict];
BOOL notifyOnEntry = [self.class boolValueFrom:regionDict[@"notifyOnEntry"] defaultValue:YES];
BOOL notifyOnExit = [self.class boolValueFrom:regionDict[@"notifyOnExit"] defaultValue:YES];
CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:center radius:radius identifier:identifier];
region.notifyOnEntry = notifyOnEntry;
region.notifyOnExit = notifyOnExit;
[regionStates setObject:@(CLRegionStateUnknown) forKey:identifier];
[locationManager startMonitoringForRegion:region];
[locationManager requestStateForRegion:region];
}
}];
}
- (void)stopMonitoringAllRegions
{
[EXUtilities performSynchronouslyOnMainThread:^{
for (CLRegion *region in self->_locationManager.monitoredRegions) {
[self->_locationManager stopMonitoringForRegion:region];
}
}];
}
- (void)executeTaskWithRegion:(nonnull CLRegion *)region eventType:(EXGeofencingEventType)eventType
{
if ([region isKindOfClass:[CLCircularRegion class]]) {
CLCircularRegion *circularRegion = (CLCircularRegion *)region;
CLRegionState regionState = [self regionStateForIdentifier:circularRegion.identifier];
NSDictionary *data = @{
@"eventType": @(eventType),
@"region": [[self class] exportRegion:circularRegion withState:regionState],
};
[_task executeWithData:data withError:nil];
}
}
# pragma mark - CLLocationManagerDelegate
// There is a bug in iOS that causes didEnterRegion and didExitRegion to be called multiple times.
// https://stackoverflow.com/questions/36807060/region-monitoring-method-getting-called-multiple-times-in-geo-fencing
// To prevent this behavior, we execute tasks only when the state has changed.
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
if ([self regionStateForIdentifier:region.identifier] != CLRegionStateInside) {
[self setRegionState:CLRegionStateInside forIdentifier:region.identifier];
[self executeTaskWithRegion:region eventType:EXGeofencingEventTypeEnter];
}
}
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
if ([self regionStateForIdentifier:region.identifier] != CLRegionStateOutside) {
[self setRegionState:CLRegionStateOutside forIdentifier:region.identifier];
[self executeTaskWithRegion:region eventType:EXGeofencingEventTypeExit];
}
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
[_task executeWithData:nil withError:error];
}
- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error
{
if (error && error.domain == kCLErrorDomain) {
// This error might happen when the device is not able to find out the location. Try to restart monitoring this region.
[_locationManager stopMonitoringForRegion:region];
[_locationManager startMonitoringForRegion:region];
[_locationManager requestStateForRegion:region];
}
}
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
{
if ([self regionStateForIdentifier:region.identifier] != state) {
EXGeofencingEventType eventType = state == CLRegionStateInside ? EXGeofencingEventTypeEnter : EXGeofencingEventTypeExit;
[self setRegionState:state forIdentifier:region.identifier];
[self executeTaskWithRegion:region eventType:eventType];
}
}
# pragma mark - helpers
- (CLRegionState)regionStateForIdentifier:(NSString *)identifier
{
return [_regionStates[identifier] integerValue];
}
- (void)setRegionState:(CLRegionState)regionState forIdentifier:(NSString *)identifier
{
[_regionStates setObject:@(regionState) forKey:identifier];
}
# pragma mark - static helpers
+ (nonnull NSDictionary *)exportRegion:(nonnull CLCircularRegion *)region withState:(CLRegionState)regionState
{
return @{
@"identifier": region.identifier,
@"state": @([self exportRegionState:regionState]),
@"radius": @(region.radius),
@"latitude": @(region.center.latitude),
@"longitude": @(region.center.longitude),
};
}
+ (EXGeofencingRegionState)exportRegionState:(CLRegionState)regionState
{
switch (regionState) {
case CLRegionStateUnknown:
return EXGeofencingRegionStateUnknown;
case CLRegionStateInside:
return EXGeofencingRegionStateInside;
case CLRegionStateOutside:
return EXGeofencingRegionStateOutside;
}
}
+ (CLLocationCoordinate2D)coordinateFromDictionary:(nonnull NSDictionary *)dict
{
CLLocationDegrees latitude = [dict[@"latitude"] doubleValue];
CLLocationDegrees longitude = [dict[@"longitude"] doubleValue];
return CLLocationCoordinate2DMake(latitude, longitude);
}
+ (BOOL)boolValueFrom:(id)pointer defaultValue:(BOOL)defaultValue
{
return pointer == nil ? defaultValue : [pointer boolValue];
}
@end
@@ -0,0 +1,14 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocationManagerDelegate.h>
#import <ExpoModulesCore/EXTaskConsumerInterface.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXLocationTaskConsumer : NSObject <EXTaskConsumerInterface, CLLocationManagerDelegate>
@property (nonatomic, strong) id<EXTaskInterface> task;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,189 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocationManager.h>
#import <CoreLocation/CLErrorDomain.h>
#import <ExpoModulesCore/EXUtilities.h>
#import <ExpoModulesCore/EXTaskInterface.h>
#import <ExpoLocation/EXLocation.h>
#import <ExpoLocation/EXLocationTaskConsumer.h>
@interface EXLocationTaskConsumer ()
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) NSMutableArray<CLLocation *> *deferredLocations;
@property (nonatomic, strong) CLLocation *lastReportedLocation;
@property (nonatomic, assign) CLLocationDistance deferredDistance;
@end
@implementation EXLocationTaskConsumer
- (instancetype)init
{
if (self = [super init]) {
_deferredLocations = [NSMutableArray new];
_deferredDistance = 0.0;
}
return self;
}
- (void)dealloc
{
[self reset];
}
# pragma mark - EXTaskConsumerInterface
- (NSString *)taskType
{
return @"location";
}
- (void)didRegisterTask:(id<EXTaskInterface>)task
{
[EXUtilities performSynchronouslyOnMainThread:^{
CLLocationManager *locationManager = [CLLocationManager new];
self->_task = task;
self->_locationManager = locationManager;
locationManager.delegate = self;
locationManager.allowsBackgroundLocationUpdates = YES;
// Set options-specific things in location manager.
[self setOptions:task.options];
}];
}
- (void)didUnregister
{
[self reset];
}
- (void)setOptions:(NSDictionary *)options
{
[EXUtilities performSynchronouslyOnMainThread:^{
CLLocationManager *locationManager = self->_locationManager;
EXLocationAccuracy accuracy = [options[@"accuracy"] unsignedIntegerValue] ?: EXLocationAccuracyBalanced;
locationManager.desiredAccuracy = [EXLocation CLLocationAccuracyFromOption:accuracy];
locationManager.distanceFilter = [self numberToDouble:options[@"distanceInterval"] defaultValue:kCLDistanceFilterNone];
locationManager.activityType = [EXLocation CLActivityTypeFromOption:[self numberToInteger:options[@"activityType"] defaultValue:CLActivityTypeOther]];
locationManager.pausesLocationUpdatesAutomatically = [self numberToBool:options[@"pausesUpdatesAutomatically"] defaultValue:true];
locationManager.showsBackgroundLocationIndicator = [self numberToBool:options[@"showsBackgroundLocationIndicator"] defaultValue:false];
[locationManager startUpdatingLocation];
[locationManager startMonitoringSignificantLocationChanges];
}];
}
# pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
{
if (_task != nil && locations.count > 0) {
[self deferLocations:locations];
[self maybeReportDeferredLocations];
}
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
[_task executeWithData:nil withError:error];
}
# pragma mark - internal
- (void)reset
{
[EXUtilities performSynchronouslyOnMainThread:^{
[self->_locationManager stopUpdatingLocation];
[self->_locationManager stopMonitoringSignificantLocationChanges];
[self->_deferredLocations removeAllObjects];
self->_lastReportedLocation = nil;
self->_deferredDistance = 0.0;
self->_locationManager = nil;
self->_task = nil;
}];
}
- (void)executeTaskWithDeferredLocations
{
// Execute task with deferred locations.
NSDictionary *data = @{ @"locations": [EXLocationTaskConsumer _exportLocations:_deferredLocations] };
[_task executeWithData:data withError:nil];
// Reset deferring state.
_lastReportedLocation = _deferredLocations.lastObject;
_deferredDistance = 0.0;
[_deferredLocations removeAllObjects];
}
- (void)maybeReportDeferredLocations
{
if ([self shouldReportDeferredLocations]) {
[self executeTaskWithDeferredLocations];
}
}
- (void)deferLocations:(NSArray<CLLocation *> *)locations
{
CLLocation *lastLocation = _deferredLocations.lastObject ?: _lastReportedLocation;
for (CLLocation *location in locations) {
if (lastLocation) {
_deferredDistance += [location distanceFromLocation:lastLocation];
}
lastLocation = location;
}
[_deferredLocations addObjectsFromArray:locations];
}
- (BOOL)shouldReportDeferredLocations
{
if (_deferredLocations.count <= 0) {
return NO;
}
UIApplicationState appState = [[UIApplication sharedApplication] applicationState];
if (appState == UIApplicationStateActive) {
// Don't defer location updates when app is in foreground state.
return YES;
}
CLLocation *oldestLocation = _lastReportedLocation ?: _deferredLocations.firstObject;
CLLocation *newestLocation = _deferredLocations.lastObject;
NSDictionary *options = _task.options;
CLLocationDistance distance = [self numberToDouble:options[@"deferredUpdatesDistance"] defaultValue:0];
NSTimeInterval interval = [self numberToDouble:options[@"deferredUpdatesInterval"] defaultValue:0];
return [newestLocation.timestamp timeIntervalSinceDate:oldestLocation.timestamp] >= interval / 1000.0 && _deferredDistance >= distance;
}
- (double)numberToDouble:(NSNumber *)number defaultValue:(double)defaultValue
{
return [number isEqual:[NSNull null]] || number == nil ? defaultValue : [number doubleValue];
}
- (NSInteger)numberToInteger:(NSNumber *)number defaultValue:(NSInteger)defaultValue
{
return [number isEqual:[NSNull null]] || number == nil ? defaultValue : [number integerValue];
}
- (BOOL)numberToBool:(NSNumber *)number defaultValue:(BOOL)defaultValue
{
return [number isEqual:[NSNull null]] || number == nil ? defaultValue : [number boolValue];
}
+ (NSArray<NSDictionary *> *)_exportLocations:(NSArray<CLLocation *> *)locations
{
NSMutableArray<NSDictionary *> *result = [NSMutableArray new];
for (CLLocation *location in locations) {
[result addObject:[EXLocation exportLocation:location]];
}
return result;
}
@end
@@ -0,0 +1 @@
92bdf64a7d26f40abe06efdd8a75fb5aea081a5b
@@ -0,0 +1 @@
6cee5095a2544eab12b9238d3508e6f84472d5d4456f3ae025d2ca0004d5ac9f
@@ -0,0 +1 @@
ea8ee4997b883d87862e4bf9be7f825c134db18f7bf963e6eef6bc58009fdf681db683d96abd99d8f7e8b3e77668496ff242690db54ea9dd21b67e6f93cd6990
@@ -0,0 +1 @@
8a53da89f23832626eae5eb2e59b5526ab1131bd
@@ -0,0 +1 @@
3fa329ef5fdb8e139c38e5ab450b9249dc3c4c5efa3ca113387eda78d2a73fa8
@@ -0,0 +1 @@
ead6b004b911d987cbcc65cc6bb08eec9cea93ec7dc073410980579b0bc55c7cdddd2799115b2fbff69d35191ea6da20f7363c5f2854df6621c89d3a8892264e
@@ -0,0 +1,110 @@
{
"formatVersion": "1.1",
"component": {
"group": "host.exp.exponent",
"module": "expo.modules.location",
"version": "19.0.8",
"attributes": {
"org.gradle.status": "release"
}
},
"createdBy": {
"gradle": {
"version": "8.14.3"
}
},
"variants": [
{
"name": "releaseVariantReleaseApiPublication",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.libraryelements": "aar",
"org.gradle.usage": "java-api"
},
"dependencies": [
{
"group": "com.google.android.gms",
"module": "play-services-location",
"version": {
"requires": "21.0.1"
}
}
],
"files": [
{
"name": "expo.modules.location-19.0.8.aar",
"url": "expo.modules.location-19.0.8.aar",
"size": 186591,
"sha512": "ead6b004b911d987cbcc65cc6bb08eec9cea93ec7dc073410980579b0bc55c7cdddd2799115b2fbff69d35191ea6da20f7363c5f2854df6621c89d3a8892264e",
"sha256": "3fa329ef5fdb8e139c38e5ab450b9249dc3c4c5efa3ca113387eda78d2a73fa8",
"sha1": "8a53da89f23832626eae5eb2e59b5526ab1131bd",
"md5": "79e64ee6876bac3d21fe620459b9e82c"
}
]
},
{
"name": "releaseVariantReleaseRuntimePublication",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.libraryelements": "aar",
"org.gradle.usage": "java-runtime"
},
"dependencies": [
{
"group": "org.jetbrains.kotlin",
"module": "kotlin-stdlib-jdk7",
"version": {
"requires": "2.1.20"
}
},
{
"group": "androidx.annotation",
"module": "annotation",
"version": {
"requires": "1.7.1"
}
},
{
"group": "com.google.android.gms",
"module": "play-services-location",
"version": {
"requires": "21.0.1"
}
}
],
"files": [
{
"name": "expo.modules.location-19.0.8.aar",
"url": "expo.modules.location-19.0.8.aar",
"size": 186591,
"sha512": "ead6b004b911d987cbcc65cc6bb08eec9cea93ec7dc073410980579b0bc55c7cdddd2799115b2fbff69d35191ea6da20f7363c5f2854df6621c89d3a8892264e",
"sha256": "3fa329ef5fdb8e139c38e5ab450b9249dc3c4c5efa3ca113387eda78d2a73fa8",
"sha1": "8a53da89f23832626eae5eb2e59b5526ab1131bd",
"md5": "79e64ee6876bac3d21fe620459b9e82c"
}
]
},
{
"name": "releaseVariantReleaseSourcePublication",
"attributes": {
"org.gradle.category": "documentation",
"org.gradle.dependency.bundling": "external",
"org.gradle.docstype": "sources",
"org.gradle.usage": "java-runtime"
},
"files": [
{
"name": "expo.modules.location-19.0.8-sources.jar",
"url": "expo.modules.location-19.0.8-sources.jar",
"size": 22916,
"sha512": "ea8ee4997b883d87862e4bf9be7f825c134db18f7bf963e6eef6bc58009fdf681db683d96abd99d8f7e8b3e77668496ff242690db54ea9dd21b67e6f93cd6990",
"sha256": "6cee5095a2544eab12b9238d3508e6f84472d5d4456f3ae025d2ca0004d5ac9f",
"sha1": "92bdf64a7d26f40abe06efdd8a75fb5aea081a5b",
"md5": "7901689a7c34acc7ae8af3462616529e"
}
]
}
]
}
@@ -0,0 +1 @@
47a13a56e46779bf36c3413b52cb1a6201df7f82
@@ -0,0 +1 @@
bdce94afa4bee2ab63896ec8a7a5297626f6a43cd559043cc78124fa8333b2b0
@@ -0,0 +1 @@
5676299fe137949faf5c5a71f7e021e8350ad4383e29af8b9ec7eb5b3839e08f586a54de09bf63966ab4b0ea20d31c28baa4ca0b4d491ae5f897912e686732e5
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>host.exp.exponent</groupId>
<artifactId>expo.modules.location</artifactId>
<version>19.0.8</version>
<packaging>aar</packaging>
<name>expo.modules.location</name>
<url>https://github.com/expo/expo</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://github.com/expo/expo/blob/main/LICENSE</url>
</license>
</licenses>
<scm>
<connection>https://github.com/expo/expo.git</connection>
<developerConnection>https://github.com/expo/expo.git</developerConnection>
<url>https://github.com/expo/expo</url>
</scm>
<dependencies>
<dependency>
<groupId>com.google.android.gms</groupId>
<artifactId>play-services-location</artifactId>
<version>21.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk7</artifactId>
<version>2.1.20</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>androidx.annotation</groupId>
<artifactId>annotation</artifactId>
<version>1.7.1</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>

Some files were not shown because too many files have changed in this diff Show More