feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,2 @@
// @generated by expo-module-scripts
module.exports = require('expo-module-scripts/eslintrc.base.js');

View File

@@ -0,0 +1,372 @@
# Changelog
## Unpublished
### 🛠 Breaking changes
### 🎉 New features
### 🐛 Bug fixes
### 💡 Others
## 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._

View File

@@ -0,0 +1,65 @@
<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 main branch](https://github.com/expo/expo/blob/main/docs/pages/versions/unversioned/sdk/location.mdx)
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/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 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.
### 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).
# Contributing
Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).

View File

@@ -0,0 +1,26 @@
apply plugin: 'com.android.library'
group = 'host.exp.exponent'
version = '17.0.1'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useDefaultAndroidSdkVersions()
useExpoPublishing()
android {
namespace "expo.modules.location"
defaultConfig {
versionCode 29
versionName "17.0.1"
}
}
dependencies {
api 'com.google.android.gms:play-services-location:21.0.1'
api('io.nlopez.smartlocation:library:3.3.3') {
transitive = false
}
}

View File

@@ -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>

View File

@@ -0,0 +1,5 @@
package expo.modules.location
interface LocationActivityResultListener {
fun onResult(resultCode: Int)
}

View File

@@ -0,0 +1,63 @@
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 MissingActivityManagerException :
CodedException("Activity manager is unavailable")
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")

View File

@@ -0,0 +1,230 @@
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 io.nlopez.smartlocation.location.config.LocationAccuracy
import io.nlopez.smartlocation.location.config.LocationParams
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 locationParamsBuilder = buildLocationParamsForAccuracy(accuracy)
options.timeInterval?.let {
locationParamsBuilder.setInterval(it)
}
options.distanceInterval?.let {
locationParamsBuilder.setDistance(it.toFloat())
}
return locationParamsBuilder.build()
}
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.Builder {
return when (accuracy) {
LocationModule.ACCURACY_LOWEST -> LocationParams.Builder()
.setAccuracy(LocationAccuracy.LOWEST)
.setDistance(3000f)
.setInterval(10000)
LocationModule.ACCURACY_LOW -> LocationParams.Builder()
.setAccuracy(LocationAccuracy.LOW)
.setDistance(1000f)
.setInterval(5000)
LocationModule.ACCURACY_BALANCED -> LocationParams.Builder()
.setAccuracy(LocationAccuracy.MEDIUM)
.setDistance(100f)
.setInterval(3000)
LocationModule.ACCURACY_HIGH -> LocationParams.Builder()
.setAccuracy(LocationAccuracy.HIGH)
.setDistance(50f)
.setInterval(2000)
LocationModule.ACCURACY_HIGHEST -> LocationParams.Builder()
.setAccuracy(LocationAccuracy.HIGH)
.setDistance(25f)
.setInterval(1000)
LocationModule.ACCURACY_BEST_FOR_NAVIGATION -> LocationParams.Builder()
.setAccuracy(LocationAccuracy.HIGH)
.setDistance(0f)
.setInterval(500)
else -> LocationParams.Builder()
.setAccuracy(LocationAccuracy.MEDIUM)
.setDistance(100f)
.setInterval(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
}

View File

@@ -0,0 +1,840 @@
package expo.modules.location
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.IntentSender.SendIntentException
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.os.Build
import android.os.Bundle
import android.os.Looper
import android.util.Log
import androidx.annotation.ChecksSdkIntAtLeast
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.ActivityProvider
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 io.nlopez.smartlocation.SmartLocation
import io.nlopez.smartlocation.geocoding.utils.LocationAddress
import io.nlopez.smartlocation.location.config.LocationParams
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 lateinit var mActivityProvider: ActivityProvider
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()
mActivityProvider = appContext.legacyModule<ActivityProvider>()
?: throw MissingActivityManagerException()
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") {
val state = SmartLocation.with(mContext).location().state()
return@AsyncFunction LocationProviderStatus().apply {
backgroundModeEnabled = state.locationServicesEnabled()
gpsAvailable = state.isGpsAvailable
networkAvailable = state.isNetworkAvailable
locationServicesEnabled = state.locationServicesEnabled()
passiveAvailable = state.isPassiveAvailable
}
}
AsyncFunction("watchDeviceHeading") { watchId: Int ->
mHeadingId = watchId
startHeadingUpdate()
return@AsyncFunction
}
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)
}
return@AsyncFunction
}
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(
scope = accuracy,
accuracy = accuracy
)
return locationPermission
} ?: throw NoPermissionsModuleException()
}
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 activity = mActivityProvider.currentActivity
if (activity == null) {
// Activity not found. It could have been called in a headless mode.
executePendingRequests(Activity.RESULT_CANCELED)
return
}
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(activity, CHECK_SETTINGS_REQUEST_CODE)
} catch (e: SendIntentException) {
// 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 locationControl = SmartLocation.with(mContext).location().oneFix().config(LocationParams.BEST_EFFORT)
val currLoc = locationControl.lastLocation
if (currLoc != null) {
mGeofield = GeomagneticField(
currLoc.latitude.toFloat(), currLoc.longitude.toFloat(), currLoc.altitude.toFloat(),
System.currentTimeMillis()
)
} else {
locationControl.start { location: Location ->
mGeofield = GeomagneticField(
location.latitude.toFloat(), location.longitude.toFloat(), location.altitude.toFloat(),
System.currentTimeMillis()
)
}
}
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()) {
SmartLocation.with(mContext).geocoding().stop()
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 locationAddress = LocationAddress(address)
GeocodeResponse.from(locationAddress.location)
}
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?) {}
}

View File

@@ -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) {}
}

View File

@@ -0,0 +1,101 @@
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() = mutableMapOf(
"accuracy" to accuracy,
"distanceInterval" to distanceInterval,
"mayShowUserSettingsDialog" to mayShowUserSettingsDialog,
"timeInterval" to timeInterval,
"deferredUpdatesDistance" to deferredUpdatesDistance,
"deferredUpdatesInterval" to deferredUpdatesInterval,
"deferredUpdatesTimeout" to deferredUpdatesTimeout,
"foregroundService" to (foregroundService?.toMutableMap() ?: mutableMapOf())
)
}
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
)
}

View File

@@ -0,0 +1,220 @@
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 scope: String,
@Field var accuracy: String
) : Record, Serializable {
constructor(bundle: Bundle) : this(
scope = (bundle.getString("accuracy") ?: "none"),
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()
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -0,0 +1 @@
module.exports = require('./plugin/build/withLocation');

View File

@@ -0,0 +1,3 @@
declare const _default: any;
export default _default;
//# sourceMappingURL=ExpoLocation.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExpoLocation.d.ts","sourceRoot":"","sources":["../src/ExpoLocation.ts"],"names":[],"mappings":";AACA,wBAAmD"}

View File

@@ -0,0 +1,3 @@
import { requireNativeModule } from 'expo-modules-core';
export default requireNativeModule('ExpoLocation');
//# sourceMappingURL=ExpoLocation.js.map

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"]}

View File

@@ -0,0 +1,31 @@
import { PermissionResponse } from 'expo-modules-core';
import { LocationLastKnownOptions, LocationObject, LocationOptions } from './Location.types';
/**
* 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.
*/
declare function getPermissionsAsync(shouldAsk?: boolean): Promise<PermissionResponse>;
declare const _default: {
getProviderStatusAsync(): Promise<{
locationServicesEnabled: boolean;
}>;
getLastKnownPositionAsync(options?: LocationLastKnownOptions): Promise<LocationObject | null>;
getCurrentPositionAsync(options: LocationOptions): Promise<LocationObject>;
removeWatchAsync(watchId: any): Promise<void>;
watchDeviceHeading(headingId: any): Promise<void>;
hasServicesEnabledAsync(): Promise<boolean>;
geocodeAsync(): Promise<any[]>;
reverseGeocodeAsync(): Promise<any[]>;
watchPositionImplAsync(watchId: string, options: LocationOptions): Promise<string>;
getPermissionsAsync: typeof getPermissionsAsync;
requestPermissionsAsync(): Promise<PermissionResponse>;
requestForegroundPermissionsAsync(): Promise<PermissionResponse>;
requestBackgroundPermissionsAsync(): Promise<PermissionResponse>;
getForegroundPermissionsAsync(): Promise<PermissionResponse>;
getBackgroundPermissionsAsync(): Promise<PermissionResponse>;
startObserving(): void;
stopObserving(): void;
};
export default _default;
//# sourceMappingURL=ExpoLocation.web.d.ts.map

View File

@@ -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;AA2C1B;;;;GAIG;AACH,iBAAe,mBAAmB,CAAC,SAAS,UAAQ,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkEjF;;8BAKiC,QAAQ;QAAE,uBAAuB,EAAE,OAAO,CAAA;KAAE,CAAC;wCAMlE,wBAAwB,GAChC,QAAQ,cAAc,GAAG,IAAI,CAAC;qCAMM,eAAe,GAAG,QAAQ,cAAc,CAAC;oCAa/C,QAAQ,IAAI,CAAC;wCAGT,QAAQ,IAAI,CAAC;+BAGjB,QAAQ,OAAO,CAAC;oBAG3B,QAAQ,GAAG,EAAE,CAAC;2BAGP,QAAQ,GAAG,EAAE,CAAC;oCAGL,MAAM,WAAW,eAAe,GAAG,QAAQ,MAAM,CAAC;;+BAoBvD,QAAQ,kBAAkB,CAAC;yCAGjB,QAAQ,kBAAkB,CAAC;yCAG3B,QAAQ,kBAAkB,CAAC;qCAG/B,QAAQ,kBAAkB,CAAC;qCAG3B,QAAQ,kBAAkB,CAAC;;;;AA1EpE,wBAiFE"}

View File

@@ -0,0 +1,177 @@
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) => {
// @ts-ignore: the types here need to be fixed
watchId = global.navigator.geolocation.watchPosition((position) => {
lastKnownPosition = geolocationPositionToJSON(position);
LocationEventEmitter.emit('Expo.locationChanged', {
watchId,
location: lastKnownPosition,
});
}, undefined,
// @ts-ignore: the options object needs to be fixed
options);
resolve(watchId);
});
},
getPermissionsAsync,
async requestPermissionsAsync() {
return getPermissionsAsync(true);
},
async requestForegroundPermissionsAsync() {
return getPermissionsAsync(true);
},
async requestBackgroundPermissionsAsync() {
return getPermissionsAsync(true);
},
async getForegroundPermissionsAsync() {
return getPermissionsAsync();
},
async getBackgroundPermissionsAsync() {
return getPermissionsAsync();
},
// no-op
startObserving() { },
stopObserving() { },
};
//# sourceMappingURL=ExpoLocation.web.js.map

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -0,0 +1 @@
{"version":3,"file":"GeolocationPolyfill.d.ts","sourceRoot":"","sources":["../src/GeolocationPolyfill.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,wBAAgB,6BAA6B,IAAI,IAAI,CAqBpD"}

View File

@@ -0,0 +1,62 @@
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,
// We don't polyfill stopObserving, this is an internal method that probably should not even exist
// in react-native docs
stopObserving: () => { },
};
}
}
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

View File

@@ -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;QACzB,+DAA+D;QAC/D,IAAI,CAAC,CAAC,QAAQ,IAAI,MAAM,CAAC,EAAE;YACzB,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;SACxB;QACD,IAAI,CAAC,CAAC,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,EAAE;YACnC,MAAM,CAAC,MAAM,CAAC,SAAS,GAAG,EAAE,CAAC;SAC9B;QAED,aAAa;QACb,MAAM,CAAC,SAAS,CAAC,WAAW,GAAG;YAC7B,kBAAkB;YAClB,aAAa;YACb,UAAU;YAEV,kGAAkG;YAClG,uBAAuB;YACvB,aAAa,EAAE,GAAG,EAAE,GAAE,CAAC;SACxB,CAAC;KACH;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;QACF,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;KACjB;IAAC,OAAO,CAAC,EAAE;QACV,KAAK,CAAC,CAAC,CAAC,CAAC;KACV;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,GAAG,EAAE,EAAE;QAClE,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 // We don't polyfill stopObserving, this is an internal method that probably should not even exist\n // in react-native docs\n stopObserving: () => {},\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) => {\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"]}

View File

@@ -0,0 +1,256 @@
import { PermissionStatus, PermissionResponse, PermissionHookOptions } from 'expo-modules-core';
import { LocationAccuracy, LocationCallback, LocationGeocodedAddress, LocationGeocodedLocation, LocationHeadingCallback, LocationHeadingObject, LocationLastKnownOptions, LocationObject, LocationOptions, LocationPermissionResponse, LocationProviderStatus, LocationRegion, LocationSubscription, LocationTaskOptions, LocationActivityType, LocationGeofencingEventType, LocationGeofencingRegionState, LocationGeocodingOptions } from './Location.types';
import { LocationEventEmitter } from './LocationEventEmitter';
import { _getCurrentWatchId } from './LocationSubscribers';
/**
* @deprecated The Geocoding web api is no longer available from SDK 49 onwards. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
* @param _apiKey Google API key obtained from Google API Console. This API key must have `Geocoding API`
* enabled, otherwise your geocoding requests will be denied.
*/
declare function setGoogleApiKey(_apiKey: string): void;
/**
* 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.
*/
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 [`Location.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. Please note that updates will only occur while the
* application is in the foreground. To get location updates while in background you'll need to use
* [Location.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.
* @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
*/
export declare function watchPositionAsync(options: LocationOptions, callback: LocationCallback): 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.
* @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
*/
export declare function watchHeadingAsync(callback: LocationHeadingCallback): Promise<LocationSubscription>;
/**
* Geocode an address string to latitude-longitude location.
* > **Note**: Using the Geocoding web api is no longer supported. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
*
* > **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.
*
* > On Android, you must request a location permission (`Permissions.LOCATION`) from the user
* > before geocoding can be used.
* @param address A string representing address, eg. `"Baker Street London"`.
* @param options
* @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedLocation`](#locationgeocodedlocation) objects.
*/
export declare function geocodeAsync(address: string, options?: LocationGeocodingOptions): Promise<LocationGeocodedLocation[]>;
/**
* Reverse geocode a location to postal address.
* > **Note**: Using the Geocoding web api is no longer supported. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
*
* > **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.
*
* > On Android, you must request a location permission (`Permissions.LOCATION`) from the user
* > before geocoding can be used.
* @param location An object representing a location.
* @param options
* @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedAddress`](#locationgeocodedaddress) objects.
*/
export declare function reverseGeocodeAsync(location: Pick<LocationGeocodedLocation, 'latitude' | 'longitude'>, options?: LocationGeocodingOptions): Promise<LocationGeocodedAddress[]>;
/**
* Checks user's permissions for accessing location.
* @return A promise that fulfills with an object of type [LocationPermissionResponse](#locationpermissionresponse).
* @deprecated Use [`getForegroundPermissionsAsync`](#locationgetforegroundpermissionsasync) or [`getBackgroundPermissionsAsync`](#locationgetbackgroundpermissionsasync) instead.
*/
export declare function getPermissionsAsync(): Promise<LocationPermissionResponse>;
/**
* Asks the user to grant permissions for location.
* @return A promise that fulfills with an object of type [LocationPermissionResponse](#locationpermissionresponse).
* @deprecated Use [`requestForegroundPermissionsAsync`](#locationrequestforegroundpermissionsasync) or [`requestBackgroundPermissionsAsync`](#locationrequestbackgroundpermissionsasync) instead.
*/
export declare function requestPermissionsAsync(): Promise<LocationPermissionResponse>;
/**
* Checks user's permissions for accessing location while the app is in the foreground.
* @return A promise that fulfills with an object of type [PermissionResponse](#permissionresponse).
*/
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 [PermissionResponse](#permissionresponse).
*/
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?: 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?: 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.
*
* ```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 geofencing 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>;
export { LocationEventEmitter as EventEmitter, _getCurrentWatchId };
export { LocationAccuracy as Accuracy, LocationActivityType as ActivityType, LocationGeofencingEventType as GeofencingEventType, LocationGeofencingRegionState as GeofencingRegionState, PermissionStatus, PermissionHookOptions, setGoogleApiKey, };
export { installWebGeolocationPolyfill } from './GeolocationPolyfill';
export * from './Location.types';
//# sourceMappingURL=Location.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"Location.d.ts","sourceRoot":"","sources":["../src/Location.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,EAGtB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,gBAAgB,EAChB,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,EACnB,oBAAoB,EACpB,2BAA2B,EAC3B,6BAA6B,EAC7B,wBAAwB,EACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAyC,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAGlG;;;;GAIG;AACH,iBAAS,eAAe,CAAC,OAAO,EAAE,MAAM,QAAI;AAG5C;;;GAGG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAE9E;AAGD;;;;GAIG;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;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAS/B;AAGD;;;;GAIG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAatE;AAGD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAAC,oBAAoB,CAAC,CAS/B;AAGD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,wBAAwB,EAAE,CAAC,CAgBrC;AAGD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,IAAI,CAAC,wBAAwB,EAAE,UAAU,GAAG,WAAW,CAAC,EAClE,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAkBpC;AAGD;;;;GAIG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAK/E;AAGD;;;;GAIG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAMnF;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,oLAGnC,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,4JAGnC,CAAC;AAKH;;;;GAIG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC,CAEhE;AAWD,wBAAsB,kCAAkC,IAAI,OAAO,CAAC,OAAO,CAAC,CAG3E;AAGD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;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;AAED,OAAO,EAAE,oBAAoB,IAAI,YAAY,EAAE,kBAAkB,EAAE,CAAC;AAEpE,OAAO,EACL,gBAAgB,IAAI,QAAQ,EAC5B,oBAAoB,IAAI,YAAY,EACpC,2BAA2B,IAAI,mBAAmB,EAClD,6BAA6B,IAAI,qBAAqB,EACtD,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,GAChB,CAAC;AAEF,OAAO,EAAE,6BAA6B,EAAE,MAAM,uBAAuB,CAAC;AACtE,cAAc,kBAAkB,CAAC"}

View File

@@ -0,0 +1,418 @@
import { PermissionStatus, createPermissionHook, Platform, } from 'expo-modules-core';
import ExpoLocation from './ExpoLocation';
import { LocationAccuracy, LocationActivityType, LocationGeofencingEventType, LocationGeofencingRegionState, } from './Location.types';
import { LocationEventEmitter } from './LocationEventEmitter';
import { LocationSubscriber, HeadingSubscriber, _getCurrentWatchId } from './LocationSubscribers';
// @needsAudit
/**
* @deprecated The Geocoding web api is no longer available from SDK 49 onwards. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
* @param _apiKey Google API key obtained from Google API Console. This API key must have `Geocoding API`
* enabled, otherwise your geocoding requests will be denied.
*/
function setGoogleApiKey(_apiKey) { }
// @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.
*/
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 [`Location.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. Please note that updates will only occur while the
* application is in the foreground. To get location updates while in background you'll need to use
* [Location.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.
* @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
*/
export async function watchPositionAsync(options, callback) {
const watchId = LocationSubscriber.registerCallback(callback);
await ExpoLocation.watchPositionImplAsync(watchId, options);
return {
remove() {
LocationSubscriber.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) => {
let tries = 0;
const subscription = await watchHeadingAsync((heading) => {
if (heading.accuracy > 1 || tries > 5) {
subscription.remove();
resolve(heading);
}
else {
tries += 1;
}
});
});
}
// @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.
* @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
*/
export async function watchHeadingAsync(callback) {
const watchId = HeadingSubscriber.registerCallback(callback);
await ExpoLocation.watchDeviceHeading(watchId);
return {
remove() {
HeadingSubscriber.unregisterCallback(watchId);
},
};
}
// @needsAudit
/**
* Geocode an address string to latitude-longitude location.
* > **Note**: Using the Geocoding web api is no longer supported. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
*
* > **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.
*
* > On Android, you must request a location permission (`Permissions.LOCATION`) from the user
* > before geocoding can be used.
* @param address A string representing address, eg. `"Baker Street London"`.
* @param options
* @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedLocation`](#locationgeocodedlocation) objects.
*/
export async function geocodeAsync(address, options) {
if (typeof address !== 'string') {
throw new TypeError(`Address to geocode must be a string. Got ${address} instead.`);
}
if (options?.useGoogleMaps || 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.
* > **Note**: Using the Geocoding web api is no longer supported. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
*
* > **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.
*
* > On Android, you must request a location permission (`Permissions.LOCATION`) from the user
* > before geocoding can be used.
* @param location An object representing a location.
* @param options
* @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedAddress`](#locationgeocodedaddress) objects.
*/
export async function reverseGeocodeAsync(location, options) {
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 (options?.useGoogleMaps || 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.
* @return A promise that fulfills with an object of type [LocationPermissionResponse](#locationpermissionresponse).
* @deprecated Use [`getForegroundPermissionsAsync`](#locationgetforegroundpermissionsasync) or [`getBackgroundPermissionsAsync`](#locationgetbackgroundpermissionsasync) instead.
*/
export async function getPermissionsAsync() {
console.warn(`"getPermissionsAsync()" is now deprecated. Please use "getForegroundPermissionsAsync()" or "getBackgroundPermissionsAsync()" instead.`);
return await ExpoLocation.getPermissionsAsync();
}
// @needsAudit
/**
* Asks the user to grant permissions for location.
* @return A promise that fulfills with an object of type [LocationPermissionResponse](#locationpermissionresponse).
* @deprecated Use [`requestForegroundPermissionsAsync`](#locationrequestforegroundpermissionsasync) or [`requestBackgroundPermissionsAsync`](#locationrequestbackgroundpermissionsasync) instead.
*/
export async function requestPermissionsAsync() {
console.warn(`"requestPermissionsAsync()" is now deprecated. Please use "requestForegroundPermissionsAsync()" or "requestBackgroundPermissionsAsync()" instead.`);
return await ExpoLocation.requestPermissionsAsync();
}
// @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 [PermissionResponse](#permissionresponse).
*/
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 [PermissionResponse](#permissionresponse).
*/
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 _validateTaskName(taskName) {
if (!taskName || typeof taskName !== 'string') {
throw new Error(`\`taskName\` must be a non-empty string. Got ${taskName} instead.`);
}
}
// @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.
*
* ```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 }) {
_validateTaskName(taskName);
await ExpoLocation.startLocationUpdatesAsync(taskName, options);
}
// @needsAudit
/**
* Stops geofencing 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) {
_validateTaskName(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) {
_validateTaskName(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 = []) {
_validateTaskName(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) {
_validateTaskName(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) {
_validateTaskName(taskName);
return ExpoLocation.hasStartedGeofencingAsync(taskName);
}
export { LocationEventEmitter as EventEmitter, _getCurrentWatchId };
export { LocationAccuracy as Accuracy, LocationActivityType as ActivityType, LocationGeofencingEventType as GeofencingEventType, LocationGeofencingRegionState as GeofencingRegionState, PermissionStatus, setGoogleApiKey, };
export { installWebGeolocationPolyfill } from './GeolocationPolyfill';
export * from './Location.types';
//# sourceMappingURL=Location.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,463 @@
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 12+
*/
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).
*/
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 11+
*/
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;
/**
* 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;
/**
* An object of options for forward and reverse geocoding.
*/
export type LocationGeocodingOptions = {
/**
* Whether to force using Google Maps API instead of the native implementation.
* Used by default only on Web platform. Requires providing an API key by `setGoogleApiKey`.
*/
useGoogleMaps?: boolean;
};
/**
* 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 = {
/**
* @deprecated Use `accuracy` field instead.
*/
scope: 'fine' | 'coarse' | 'none';
/**
* 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

View File

@@ -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;;;;OAIG;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;AAGjE;;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;;;;;OAKG;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;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAGF;;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,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClC;;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"}

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 12+
*/
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

View File

@@ -0,0 +1,3 @@
import { EventEmitter } from 'expo-modules-core';
export declare const LocationEventEmitter: EventEmitter;
//# sourceMappingURL=LocationEventEmitter.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"LocationEventEmitter.d.ts","sourceRoot":"","sources":["../src/LocationEventEmitter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIjD,eAAO,MAAM,oBAAoB,cAAiC,CAAC"}

View File

@@ -0,0 +1,4 @@
import { EventEmitter } from 'expo-modules-core';
import ExpoLocation from './ExpoLocation';
export const LocationEventEmitter = new EventEmitter(ExpoLocation);
//# sourceMappingURL=LocationEventEmitter.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"LocationEventEmitter.js","sourceRoot":"","sources":["../src/LocationEventEmitter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAE1C,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,YAAY,CAAC,YAAY,CAAC,CAAC","sourcesContent":["import { EventEmitter } from 'expo-modules-core';\n\nimport ExpoLocation from './ExpoLocation';\n\nexport const LocationEventEmitter = new EventEmitter(ExpoLocation);\n"]}

View File

@@ -0,0 +1,3 @@
import { EventEmitter } from 'expo-modules-core';
export declare const LocationEventEmitter: EventEmitter;
//# sourceMappingURL=LocationEventEmitter.web.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"LocationEventEmitter.web.d.ts","sourceRoot":"","sources":["../src/LocationEventEmitter.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,eAAO,MAAM,oBAAoB,cAA8B,CAAC"}

View File

@@ -0,0 +1,3 @@
import { EventEmitter } from 'expo-modules-core';
export const LocationEventEmitter = new EventEmitter({});
//# sourceMappingURL=LocationEventEmitter.web.js.map

View File

@@ -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,CAAC,EAAS,CAAC,CAAC","sourcesContent":["import { EventEmitter } from 'expo-modules-core';\n\nexport const LocationEventEmitter = new EventEmitter({} as any);\n"]}

View File

@@ -0,0 +1,30 @@
import { LocationCallback, LocationHeadingCallback } from './Location.types';
type EventObject = {
watchId: number;
[key: string]: any;
};
declare class Subscriber<CallbackType extends LocationCallback | LocationHeadingCallback> {
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;
/**
* 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>;
/**
* @private Necessary for some unit tests.
*/
export declare function _getCurrentWatchId(): number;
export {};
//# sourceMappingURL=LocationSubscribers.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"LocationSubscribers.d.ts","sourceRoot":"","sources":["../src/LocationSubscribers.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAG7E,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAC;AAIF,cAAM,UAAU,CAAC,YAAY,SAAS,gBAAgB,GAAG,uBAAuB;IAC9E,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,iBAAiB,CAA6B;gBAE1C,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;IAKrD,2BAA2B;IAU3B;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM;IAOhD;;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;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}

View File

@@ -0,0 +1,62 @@
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;
}
/**
* 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');
/**
* @private Necessary for some unit tests.
*/
export function _getCurrentWatchId() {
return nextWatchId;
}
//# sourceMappingURL=LocationSubscribers.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"LocationSubscribers.js","sourceRoot":"","sources":["../src/LocationSubscribers.ts"],"names":[],"mappings":"AAEA,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAE1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAO9D,IAAI,WAAW,GAAG,CAAC,CAAC;AAEpB,MAAM,UAAU;IACN,SAAS,CAAS;IAClB,cAAc,CAAS;IACvB,SAAS,GAAmC,EAAE,CAAC;IAC/C,iBAAiB,GAAwB,IAAI,CAAC;IAEtD,YAAY,SAAiB,EAAE,cAAsB;QACnD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;IACvC,CAAC;IAED,2BAA2B;QACzB,IAAI,IAAI,CAAC,iBAAiB,EAAE;YAC1B,OAAO;SACR;QACD,IAAI,CAAC,iBAAiB,GAAG,oBAAoB,CAAC,WAAW,CACvD,IAAI,CAAC,SAAS,EACd,CAAC,KAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAC5C,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,gBAAgB,CAAC,QAAsB;QACrC,IAAI,CAAC,2BAA2B,EAAE,CAAC;QACnC,MAAM,EAAE,GAAG,EAAE,WAAW,CAAC;QACzB,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC;QAC9B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED;;OAEG;IACH,kBAAkB,CAAC,EAAU;QAC3B,2DAA2D;QAC3D,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE;YACvB,OAAO;SACR;QAED,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAC1B,YAAY,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC;QAElC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,iBAAiB,EAAE;YACtE,oBAAoB,CAAC,kBAAkB,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;YAChE,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;SAC/B;IACH,CAAC;IAED,OAAO,CAAC,KAAkB;QACxB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAEzC,IAAI,QAAQ,EAAE;YACZ,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;SACtC;aAAM;YACL,YAAY,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;SACxC;IACH,CAAC;CACF;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,UAAU,CAC9C,sBAAsB,EACtB,UAAU,CACX,CAAC;AACF,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,UAAU,CAC7C,qBAAqB,EACrB,SAAS,CACV,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,WAAW,CAAC;AACrB,CAAC","sourcesContent":["import { Subscription } from 'expo-modules-core';\n\nimport ExpoLocation from './ExpoLocation';\nimport { LocationCallback, LocationHeadingCallback } from './Location.types';\nimport { LocationEventEmitter } from './LocationEventEmitter';\n\ntype EventObject = {\n watchId: number;\n [key: string]: any;\n};\n\nlet nextWatchId = 0;\n\nclass Subscriber<CallbackType extends LocationCallback | LocationHeadingCallback> {\n private eventName: string;\n private eventDataField: string;\n private callbacks: { [id: string]: CallbackType } = {};\n private eventSubscription: Subscription | null = null;\n\n constructor(eventName: string, eventDataField: string) {\n this.eventName = eventName;\n this.eventDataField = eventDataField;\n }\n\n maybeInitializeSubscription() {\n if (this.eventSubscription) {\n return;\n }\n this.eventSubscription = LocationEventEmitter.addListener(\n this.eventName,\n (event: EventObject) => this.trigger(event)\n );\n }\n\n /**\n * Registers given callback under new id which is then returned.\n */\n registerCallback(callback: CallbackType): number {\n this.maybeInitializeSubscription();\n const id = ++nextWatchId;\n this.callbacks[id] = callback;\n return id;\n }\n\n /**\n * Unregisters a callback with given id and revokes the subscription if possible.\n */\n unregisterCallback(id: number): void {\n // Do nothing if we have already unregistered the callback.\n if (!this.callbacks[id]) {\n return;\n }\n\n delete this.callbacks[id];\n ExpoLocation.removeWatchAsync(id);\n\n if (Object.keys(this.callbacks).length === 0 && this.eventSubscription) {\n LocationEventEmitter.removeSubscription(this.eventSubscription);\n this.eventSubscription = null;\n }\n }\n\n trigger(event: EventObject): void {\n const watchId = event.watchId;\n const callback = this.callbacks[watchId];\n\n if (callback) {\n callback(event[this.eventDataField]);\n } else {\n ExpoLocation.removeWatchAsync(watchId);\n }\n }\n}\n\nexport const LocationSubscriber = new Subscriber<LocationCallback>(\n 'Expo.locationChanged',\n 'location'\n);\nexport const HeadingSubscriber = new Subscriber<LocationHeadingCallback>(\n 'Expo.headingChanged',\n 'heading'\n);\n\n/**\n * @private Necessary for some unit tests.\n */\nexport function _getCurrentWatchId(): number {\n return nextWatchId;\n}\n"]}

View File

@@ -0,0 +1,6 @@
{
"platforms": ["ios", "android"],
"android": {
"modules": ["expo.modules.location.LocationModule"]
}
}

View File

@@ -0,0 +1,25 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
Pod::Spec.new do |s|
s.name = 'EXLocation'
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.platform = :ios, '13.4'
s.source = { git: 'https://github.com/expo/expo.git' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0')
s.source_files = "#{s.name}/**/*.h"
s.vendored_frameworks = "#{s.name}.xcframework"
else
s.source_files = "#{s.name}/**/*.{h,m}"
end
end

View File

@@ -0,0 +1,39 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocation.h>
#import <CoreLocation/CLLocationManager.h>
#import <ExpoModulesCore/EXEventEmitter.h>
#import <ExpoModulesCore/EXExportedModule.h>
#import <ExpoModulesCore/EXModuleRegistryConsumer.h>
// Location accuracies
typedef NS_ENUM(NSUInteger, EXLocationAccuracy) {
EXLocationAccuracyLowest = 1,
EXLocationAccuracyLow = 2,
EXLocationAccuracyBalanced = 3,
EXLocationAccuracyHigh = 4,
EXLocationAccuracyHighest = 5,
EXLocationAccuracyBestForNavigation = 6,
};
// 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 EXLocation : EXExportedModule <EXEventEmitter, EXModuleRegistryConsumer>
+ (NSDictionary *)exportLocation:(CLLocation *)location;
+ (CLLocationAccuracy)CLLocationAccuracyFromOption:(EXLocationAccuracy)accuracy;
+ (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType;
@end

View File

@@ -0,0 +1,627 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/EXLocation.h>
#import <EXLocation/EXLocationDelegate.h>
#import <EXLocation/EXLocationTaskConsumer.h>
#import <EXLocation/EXGeofencingTaskConsumer.h>
#import <EXLocation/EXLocationPermissionRequester.h>
#import <EXLocation/EXForegroundPermissionRequester.h>
#import <EXLocation/EXBackgroundLocationPermissionRequester.h>
#import <CoreLocation/CLLocationManager.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
#import <CoreLocation/CLHeading.h>
#import <CoreLocation/CLGeocoder.h>
#import <CoreLocation/CLPlacemark.h>
#import <CoreLocation/CLError.h>
#import <CoreLocation/CLCircularRegion.h>
#import <ExpoModulesCore/EXEventEmitterService.h>
#import <ExpoModulesCore/EXPermissionsInterface.h>
#import <ExpoModulesCore/EXPermissionsMethodsDelegate.h>
#import <ExpoModulesCore/EXTaskManagerInterface.h>
NS_ASSUME_NONNULL_BEGIN
NSString * const EXLocationChangedEventName = @"Expo.locationChanged";
NSString * const EXHeadingChangedEventName = @"Expo.headingChanged";
@interface EXLocation ()
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, EXLocationDelegate*> *delegates;
@property (nonatomic, strong) NSMutableSet<EXLocationDelegate *> *retainedDelegates;
@property (nonatomic, weak) id<EXEventEmitterService> eventEmitter;
@property (nonatomic, weak) id<EXPermissionsInterface> permissionsManager;
@property (nonatomic, weak) id<EXTaskManagerInterface> tasksManager;
@end
@implementation EXLocation
EX_EXPORT_MODULE(ExpoLocation);
- (instancetype)init
{
if (self = [super init]) {
_delegates = [NSMutableDictionary dictionary];
_retainedDelegates = [NSMutableSet set];
}
return self;
}
- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
{
_eventEmitter = [moduleRegistry getModuleImplementingProtocol:@protocol(EXEventEmitterService)];
_tasksManager = [moduleRegistry getModuleImplementingProtocol:@protocol(EXTaskManagerInterface)];
_permissionsManager = [moduleRegistry getModuleImplementingProtocol:@protocol(EXPermissionsInterface)];
[EXPermissionsMethodsDelegate registerRequesters:@[
[EXLocationPermissionRequester new],
[EXForegroundPermissionRequester new],
[EXBackgroundLocationPermissionRequester new]
] withPermissionsManager:_permissionsManager];
}
- (dispatch_queue_t)methodQueue
{
// Location managers must be created on the main thread
return dispatch_get_main_queue();
}
# pragma mark - EXEventEmitter
- (NSArray<NSString *> *)supportedEvents
{
return @[EXLocationChangedEventName, EXHeadingChangedEventName];
}
- (void)startObserving {}
- (void)stopObserving {}
# pragma mark - Exported methods
EX_EXPORT_METHOD_AS(getProviderStatusAsync,
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
resolve(@{
@"locationServicesEnabled": @([CLLocationManager locationServicesEnabled]),
@"backgroundModeEnabled": @([_tasksManager hasBackgroundModeEnabled:@"location"]),
});
}
EX_EXPORT_METHOD_AS(getCurrentPositionAsync,
options:(NSDictionary *)options
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
if (![self checkForegroundPermissions:reject]) {
return;
}
CLLocationManager *locMgr = [self locationManagerWithOptions:options];
__weak typeof(self) weakSelf = self;
__block EXLocationDelegate *delegate;
delegate = [[EXLocationDelegate alloc] initWithId:nil withLocMgr:locMgr onUpdateLocations:^(NSArray<CLLocation *> * _Nonnull locations) {
if (delegate != nil) {
if (locations.lastObject != nil) {
resolve([EXLocation exportLocation:locations.lastObject]);
} else {
reject(@"E_LOCATION_NOT_FOUND", @"Current location not found.", nil);
}
[weakSelf.retainedDelegates removeObject:delegate];
delegate = nil;
}
} onUpdateHeadings:nil onError:^(NSError *error) {
reject(@"E_LOCATION_UNAVAILABLE", [@"Cannot obtain current location: " stringByAppendingString:error.description], nil);
}];
// retain location manager delegate so it will not dealloc until onUpdateLocations gets called
[_retainedDelegates addObject:delegate];
locMgr.delegate = delegate;
[locMgr requestLocation];
}
EX_EXPORT_METHOD_AS(watchPositionImplAsync,
watchId:(nonnull NSNumber *)watchId
options:(NSDictionary *)options
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
if (![self checkForegroundPermissions:reject]) {
return;
}
__weak typeof(self) weakSelf = self;
CLLocationManager *locMgr = [self locationManagerWithOptions:options];
EXLocationDelegate *delegate = [[EXLocationDelegate alloc] initWithId:watchId withLocMgr:locMgr onUpdateLocations:^(NSArray<CLLocation *> *locations) {
if (locations.lastObject != nil && weakSelf != nil) {
__strong typeof(weakSelf) strongSelf = weakSelf;
CLLocation *loc = locations.lastObject;
NSDictionary *body = @{
@"watchId": watchId,
@"location": [EXLocation exportLocation:loc],
};
[strongSelf->_eventEmitter sendEventWithName:EXLocationChangedEventName body:body];
}
} onUpdateHeadings:nil onError:^(NSError *error) {
// TODO: report errors
// (ben) error could be (among other things):
// - kCLErrorDenied - we should use the same UNAUTHORIZED behavior as elsewhere
// - kCLErrorLocationUnknown - we can actually ignore this error and keep tracking
// location (I think -- my knowledge might be a few months out of date)
}];
_delegates[delegate.watchId] = delegate;
locMgr.delegate = delegate;
[locMgr startUpdatingLocation];
resolve([NSNull null]);
}
EX_EXPORT_METHOD_AS(getLastKnownPositionAsync,
getLastKnownPositionWithOptions:(NSDictionary *)options
resolve:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
if (![self checkForegroundPermissions:reject]) {
return;
}
CLLocation *location = [[self locationManagerWithOptions:nil] location];
if ([self.class isLocation:location validWithOptions:options]) {
resolve([EXLocation exportLocation:location]);
} else {
resolve([NSNull null]);
}
}
// Watch method for getting compass updates
EX_EXPORT_METHOD_AS(watchDeviceHeading,
watchHeadingWithWatchId:(nonnull NSNumber *)watchId
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject) {
if (![self checkForegroundPermissions:reject]) {
return;
}
__weak typeof(self) weakSelf = self;
CLLocationManager *locMgr = [[CLLocationManager alloc] init];
locMgr.distanceFilter = kCLDistanceFilterNone;
locMgr.desiredAccuracy = kCLLocationAccuracyBest;
locMgr.allowsBackgroundLocationUpdates = NO;
EXLocationDelegate *delegate = [[EXLocationDelegate alloc] initWithId:watchId withLocMgr:locMgr onUpdateLocations: nil onUpdateHeadings:^(CLHeading *newHeading) {
if (newHeading != nil && weakSelf != nil) {
__strong typeof(weakSelf) strongSelf = weakSelf;
NSNumber *accuracy;
// Convert iOS heading accuracy to Android system
// 3: high accuracy, 2: medium, 1: low, 0: none
if (newHeading.headingAccuracy > 50 || newHeading.headingAccuracy < 0) {
accuracy = @(0);
} else if (newHeading.headingAccuracy > 35) {
accuracy = @(1);
} else if (newHeading.headingAccuracy > 20) {
accuracy = @(2);
} else {
accuracy = @(3);
}
NSDictionary *body = @{@"watchId": watchId,
@"heading": @{
@"trueHeading": @(newHeading.trueHeading),
@"magHeading": @(newHeading.magneticHeading),
@"accuracy": accuracy,
},
};
[strongSelf->_eventEmitter sendEventWithName:EXHeadingChangedEventName body:body];
}
} onError:^(NSError *error) {
// Error getting updates
}];
_delegates[delegate.watchId] = delegate;
locMgr.delegate = delegate;
[locMgr startUpdatingHeading];
resolve([NSNull null]);
}
EX_EXPORT_METHOD_AS(removeWatchAsync,
watchId:(nonnull NSNumber *)watchId
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
EXLocationDelegate *delegate = _delegates[watchId];
if (delegate) {
// Unsuscribe from both location and heading updates
[delegate.locMgr stopUpdatingLocation];
[delegate.locMgr stopUpdatingHeading];
delegate.locMgr.delegate = nil;
[_delegates removeObjectForKey:watchId];
}
resolve([NSNull null]);
}
EX_EXPORT_METHOD_AS(geocodeAsync,
address:(nonnull NSString *)address
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder geocodeAddressString:address completionHandler:^(NSArray* placemarks, NSError* error){
if (!error) {
NSMutableArray *results = [NSMutableArray arrayWithCapacity:placemarks.count];
for (CLPlacemark* placemark in placemarks) {
CLLocation *location = placemark.location;
[results addObject:@{
@"latitude": @(location.coordinate.latitude),
@"longitude": @(location.coordinate.longitude),
@"altitude": @(location.altitude),
@"accuracy": @(location.horizontalAccuracy),
}];
}
resolve(results);
} else if (error.code == kCLErrorGeocodeFoundNoResult || error.code == kCLErrorGeocodeFoundPartialResult) {
resolve(@[]);
} else if (error.code == kCLErrorNetwork) {
reject(@"E_RATE_EXCEEDED", @"Rate limit exceeded - too many requests", error);
} else {
reject(@"E_GEOCODING_FAILED", @"Error while geocoding an address", error);
}
}];
}
EX_EXPORT_METHOD_AS(reverseGeocodeAsync,
locationMap:(nonnull NSDictionary *)locationMap
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
CLLocation *location = [[CLLocation alloc] initWithLatitude:[locationMap[@"latitude"] floatValue] longitude:[locationMap[@"longitude"] floatValue]];
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray* placemarks, NSError* error){
if (!error) {
NSMutableArray *results = [NSMutableArray arrayWithCapacity:placemarks.count];
for (CLPlacemark* placemark in placemarks) {
NSDictionary *address = @{
@"city": UMNullIfNil(placemark.locality),
@"district": UMNullIfNil(placemark.subLocality),
@"streetNumber": UMNullIfNil(placemark.subThoroughfare),
@"street": EXNullIfNil(placemark.thoroughfare),
@"region": EXNullIfNil(placemark.administrativeArea),
@"subregion": EXNullIfNil(placemark.subAdministrativeArea),
@"country": EXNullIfNil(placemark.country),
@"postalCode": EXNullIfNil(placemark.postalCode),
@"name": EXNullIfNil(placemark.name),
@"isoCountryCode": EXNullIfNil(placemark.ISOcountryCode),
@"timezone": EXNullIfNil(placemark.timeZone.name),
};
[results addObject:address];
}
resolve(results);
} else if (error.code == kCLErrorGeocodeFoundNoResult || error.code == kCLErrorGeocodeFoundPartialResult) {
resolve(@[]);
} else if (error.code == kCLErrorNetwork) {
reject(@"E_RATE_EXCEEDED", @"Rate limit exceeded - too many requests", error);
} else {
reject(@"E_REVGEOCODING_FAILED", @"Error while reverse-geocoding a location", error);
}
}];
}
EX_EXPORT_METHOD_AS(getPermissionsAsync,
getPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXLocationPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(requestPermissionsAsync,
requestPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXLocationPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(getForegroundPermissionsAsync,
getForegroundPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXForegroundPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(requestForegroundPermissionsAsync,
requestForegroundPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXForegroundPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(getBackgroundPermissionsAsync,
getBackgroundPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXBackgroundLocationPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(requestBackgroundPermissionsAsync,
requestBackgroundPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXBackgroundLocationPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(hasServicesEnabledAsync,
hasServicesEnabled:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
BOOL servicesEnabled = [CLLocationManager locationServicesEnabled];
resolve(@(servicesEnabled));
}
# pragma mark - Background location
EX_EXPORT_METHOD_AS(startLocationUpdatesAsync,
startLocationUpdatesForTaskWithName:(nonnull NSString *)taskName
withOptions:(nonnull NSDictionary *)options
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
// 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.
if (![self checkForegroundPermissions:reject] || ![self checkTaskManagerExists:reject] || ![self checkBackgroundServices:reject]) {
return;
}
if (![CLLocationManager significantLocationChangeMonitoringAvailable]) {
return reject(@"E_SIGNIFICANT_CHANGES_UNAVAILABLE", @"Significant location changes monitoring is not available.", nil);
}
@try {
[_tasksManager registerTaskWithName:taskName consumer:[EXLocationTaskConsumer class] options:options];
}
@catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
EX_EXPORT_METHOD_AS(stopLocationUpdatesAsync,
stopLocationUpdatesForTaskWithName:(NSString *)taskName
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
@try {
[_tasksManager unregisterTaskWithName:taskName consumerClass:[EXLocationTaskConsumer class]];
} @catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
EX_EXPORT_METHOD_AS(hasStartedLocationUpdatesAsync,
hasStartedLocationUpdatesForTaskWithName:(nonnull NSString *)taskName
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
resolve(@([_tasksManager taskWithName:taskName hasConsumerOfClass:[EXLocationTaskConsumer class]]));
}
# pragma mark - Geofencing
EX_EXPORT_METHOD_AS(startGeofencingAsync,
startGeofencingWithTaskName:(nonnull NSString *)taskName
withOptions:(nonnull NSDictionary *)options
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![self checkBackgroundPermissions:reject] || ![self checkTaskManagerExists:reject]) {
return;
}
if (![CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
return reject(@"E_GEOFENCING_UNAVAILABLE", @"Geofencing is not available", nil);
}
@try {
[_tasksManager registerTaskWithName:taskName consumer:[EXGeofencingTaskConsumer class] options:options];
} @catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
EX_EXPORT_METHOD_AS(stopGeofencingAsync,
stopGeofencingWithTaskName:(nonnull NSString *)taskName
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
@try {
[_tasksManager unregisterTaskWithName:taskName consumerClass:[EXGeofencingTaskConsumer class]];
} @catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
EX_EXPORT_METHOD_AS(hasStartedGeofencingAsync,
hasStartedGeofencingForTaskWithName:(NSString *)taskName
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
resolve(@([_tasksManager taskWithName:taskName hasConsumerOfClass:[EXGeofencingTaskConsumer class]]));
}
# pragma mark - helpers
- (CLLocationManager *)locationManagerWithOptions:(nullable NSDictionary *)options
{
CLLocationManager *locMgr = [[CLLocationManager alloc] init];
locMgr.allowsBackgroundLocationUpdates = NO;
if (options) {
locMgr.distanceFilter = options[@"distanceInterval"] ? [options[@"distanceInterval"] doubleValue] ?: kCLDistanceFilterNone : kCLLocationAccuracyHundredMeters;
if (options[@"accuracy"]) {
EXLocationAccuracy accuracy = [options[@"accuracy"] unsignedIntegerValue] ?: EXLocationAccuracyBalanced;
locMgr.desiredAccuracy = [self.class CLLocationAccuracyFromOption:accuracy];
}
}
return locMgr;
}
- (BOOL)checkForegroundPermissions:(EXPromiseRejectBlock)reject
{
if (![CLLocationManager locationServicesEnabled]) {
reject(@"E_LOCATION_SERVICES_DISABLED", @"Location services are disabled", nil);
return NO;
}
if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXForegroundPermissionRequester class]]) {
reject(@"E_NO_PERMISSIONS", @"LOCATION_FOREGROUND permission is required to do this operation.", nil);
return NO;
}
return YES;
}
- (BOOL)checkBackgroundPermissions:(EXPromiseRejectBlock)reject
{
if (![CLLocationManager locationServicesEnabled]) {
reject(@"E_LOCATION_SERVICES_DISABLED", @"Location services are disabled", nil);
return NO;
}
if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXBackgroundLocationPermissionRequester class]]) {
reject(@"E_NO_PERMISSIONS", @"LOCATION_BACKGROUND permission is required to do this operation.", nil);
return NO;
}
return YES;
}
- (BOOL)checkTaskManagerExists:(EXPromiseRejectBlock)reject
{
if (_tasksManager == nil) {
reject(@"E_TASKMANAGER_NOT_FOUND", @"`expo-task-manager` module is required to use background services.", nil);
return NO;
}
return YES;
}
- (BOOL)checkBackgroundServices:(EXPromiseRejectBlock)reject
{
if (![_tasksManager hasBackgroundModeEnabled:@"location"]) {
reject(@"E_BACKGROUND_SERVICES_DISABLED", @"Background Location has not been configured. To enable it, add `location` to `UIBackgroundModes` in Info.plist file.", nil);
return NO;
}
return YES;
}
# pragma mark - static helpers
+ (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 (@available(iOS 12.0, *)) {
if (activityType == CLActivityTypeAirborne) {
return activityType;
}
}
return CLActivityTypeOther;
}
+ (BOOL)isLocation:(nullable CLLocation *)location validWithOptions:(nullable NSDictionary *)options
{
if (location == nil) {
return NO;
}
NSTimeInterval maxAge = options[@"maxAge"] ? [options[@"maxAge"] doubleValue] : DBL_MAX;
CLLocationAccuracy requiredAccuracy = options[@"requiredAccuracy"] ? [options[@"requiredAccuracy"] doubleValue] : DBL_MAX;
NSTimeInterval timeDiff = -location.timestamp.timeIntervalSinceNow;
return location != nil && timeDiff * 1000 <= maxAge && location.horizontalAccuracy <= requiredAccuracy;
}
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,32 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <CoreLocation/CLHeading.h>
#import <CoreLocation/CLLocation.h>
#import <CoreLocation/CLLocationManager.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXLocationDelegate : NSObject <CLLocationManagerDelegate>
@property (nonatomic, strong) NSNumber *watchId;
@property (nonatomic, strong) CLLocationManager *locMgr;
@property (nonatomic, strong) void (^onUpdateLocations)(NSArray<CLLocation *> *locations);
@property (nonatomic, strong) void (^onUpdateHeadings)(CLHeading *newHeading);
@property (nonatomic, strong) void (^onError)(NSError *error);
- (instancetype)initWithId:(nullable NSNumber *)watchId
withLocMgr:(CLLocationManager *)locMgr
onUpdateLocations:(nullable void (^)(NSArray<CLLocation *> *locations))onUpdateLocations
onUpdateHeadings:(nullable void (^)(CLHeading *newHeading))onUpdateHeadings
onError:(nullable void (^)(NSError *error))onError;
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations;
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading;
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(nonnull NSError *)error;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,46 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <EXLocation/EXLocationDelegate.h>
@implementation EXLocationDelegate
- (instancetype)initWithId:(nullable NSNumber *)watchId
withLocMgr:(CLLocationManager *)locMgr
onUpdateLocations:(nullable void (^)(NSArray<CLLocation *> *locations))onUpdateLocations
onUpdateHeadings:(nullable void (^)(CLHeading *newHeading))onUpdateHeadings
onError:(nullable void (^)(NSError *error))onError
{
if ((self = [super init])) {
_watchId = watchId;
_locMgr = locMgr;
_onUpdateLocations = onUpdateLocations;
_onUpdateHeadings = onUpdateHeadings;
_onError = onError;
}
return self;
}
// Delegate method called by CLLocationManager
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
{
if (_onUpdateLocations) {
_onUpdateLocations(locations);
}
}
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
{
if (_onUpdateHeadings) {
_onUpdateHeadings(newHeading);
}
}
// Delegate method called by CLLocationManager
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(nonnull NSError *)error
{
if (_onError) {
_onError(error);
}
}
@end

View File

@@ -0,0 +1,7 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/EXBaseLocationRequester.h>
@interface EXBackgroundLocationPermissionRequester : EXBaseLocationRequester
@end

View File

@@ -0,0 +1,99 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/EXBackgroundLocationPermissionRequester.h>
#import <objc/message.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
static SEL alwaysAuthorizationSelector;
@interface EXBackgroundLocationPermissionRequester ()
@property (nonatomic, assign) bool wasAsked;
@end
@implementation EXBackgroundLocationPermissionRequester
- (instancetype)init
{
if (self = [super init]) {
_wasAsked = 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;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleAppBecomingActive)
name:UIApplicationDidBecomeActiveNotification
object:nil];
((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;
}
}
// If user selects "Keep Only While Using" option, the `locationManagerDidChangeAuthorization` won't be called.
// So we don't know when we should resolve promise.
// Hovewer, we can check for `UIApplicationDidBecomeActiveNotification` event which is called when permissions modal disappears.
- (void)handleAppBecomingActive
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (self.resolve) {
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) };
}
@end

View File

@@ -0,0 +1,21 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocationManager.h>
#import <ExpoModulesCore/EXPermissionsInterface.h>
@interface EXBaseLocationRequester : NSObject<EXPermissionsRequester>
@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;
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status;
@end

View File

@@ -0,0 +1,164 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/EXBaseLocationRequester.h>
#import <ExpoModulesCore/EXUtilities.h>
#import <objc/message.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
@interface EXBaseLocationRequester () <CLLocationManagerDelegate>
@property (nonatomic, assign) bool locationManagerWasCalled;
@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. Why there's no way to call specifically whenInUse or always authorization?
//
// The requester sets itself as the delegate of the CLLocationManager, so when the user responds
// to a permission requesting dialog, manager calls `locationManager:didChangeAuthorizationStatus:` method.
// To be precise, manager calls this method in two circumstances:
// - right when `request*Authorization` method is called,
// - when `authorizationStatus` changes.
// With this behavior we aren't able to support the following use case:
// - app requests `whenInUse` authorization
// - user allows `whenInUse` authorization
// - `authorizationStatus` changes from `undetermined` to `whenInUse`, callback is called, promise is resolved
// - app wants to escalate authorization to `always`
// - user selects `whenInUse` authorization (iOS 11+)
// - `authorizationStatus` doesn't change, so callback is not called and requester can't know whether
// user responded to the dialog selecting `whenInUse` or is still deciding
// To support this use case we will have to change the way location authorization is requested
// from promise-based to listener-based.
[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)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
// TODO: Permissions.LOCATION issue (search by this phrase)
// if Permissions.LOCATION is being called for the first time on iOS devide and prompts for user action it might not call this callback at all
// it happens if user requests more that one permission at the same time via Permissions.askAsync(...) and LOCATION dialog is not being called first
// to reproduce this find NCL code testing that
if (status == kCLAuthorizationStatusNotDetermined || !_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;
}
}
- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager
{
if (@available(iOS 14.0, *)) {
CLAuthorizationStatus status = [manager authorizationStatus];
if (status == kCLAuthorizationStatusNotDetermined || !_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

View File

@@ -0,0 +1,8 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/EXBaseLocationRequester.h>
@interface EXForegroundPermissionRequester : EXBaseLocationRequester
@end

View File

@@ -0,0 +1,60 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/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) };
}
@end

View File

@@ -0,0 +1,7 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/EXBaseLocationRequester.h>
@interface EXLocationPermissionRequester : EXBaseLocationRequester
@end

View File

@@ -0,0 +1,72 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/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

View File

@@ -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 EXGeofencingTaskConsumer : NSObject <EXTaskConsumerInterface, CLLocationManagerDelegate>
@property (nonatomic, strong) id<EXTaskInterface> task;
@end
NS_ASSUME_NONNULL_END

View File

@@ -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 <EXLocation/EXLocation.h>
#import <EXLocation/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

View File

@@ -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

View File

@@ -0,0 +1,188 @@
// 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 <EXLocation/EXLocation.h>
#import <EXLocation/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 = [options[@"distanceInterval"] doubleValue] ?: kCLDistanceFilterNone;
locationManager.activityType = [EXLocation CLActivityTypeFromOption:[options[@"activityType"] integerValue]];
locationManager.pausesLocationUpdatesAutomatically = [options[@"pausesUpdatesAutomatically"] boolValue];
locationManager.showsBackgroundLocationIndicator = [options[@"showsBackgroundLocationIndicator"] boolValue];
[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
{
if (error.domain == kCLErrorDomain) {
// This error might happen when the device is not able to find out the location. Try to restart monitoring location.
[manager stopUpdatingLocation];
[manager stopMonitoringSignificantLocationChanges];
[manager startUpdatingLocation];
[manager startMonitoringSignificantLocationChanges];
} else {
[_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 == nil ? defaultValue : [number doubleValue];
}
+ (NSArray<NSDictionary *> *)_exportLocations:(NSArray<CLLocation *> *)locations
{
NSMutableArray<NSDictionary *> *result = [NSMutableArray new];
for (CLLocation *location in locations) {
[result addObject:[EXLocation exportLocation:location]];
}
return result;
}
@end

View File

@@ -0,0 +1,48 @@
{
"name": "expo-location",
"version": "17.0.1",
"description": "Allows reading geolocation information from the device. Your app can poll for the current location or subscribe to location update events.",
"main": "build/Location.js",
"types": "build/Location.d.ts",
"sideEffects": false,
"scripts": {
"build": "expo-module build",
"clean": "expo-module clean",
"lint": "expo-module lint",
"test": "expo-module test",
"prepare": "expo-module prepare",
"prepublishOnly": "expo-module prepublishOnly",
"expo-module": "expo-module"
},
"keywords": [
"react-native",
"expo",
"location",
"geolocation",
"coords",
"geocoding",
"compass",
"heading"
],
"repository": {
"type": "git",
"url": "https://github.com/expo/expo.git",
"directory": "packages/expo-location"
},
"bugs": {
"url": "https://github.com/expo/expo/issues"
},
"author": "650 Industries, Inc.",
"license": "MIT",
"homepage": "https://docs.expo.dev/versions/latest/sdk/location/",
"jest": {
"preset": "expo-module-scripts"
},
"devDependencies": {
"expo-module-scripts": "^3.0.0"
},
"peerDependencies": {
"expo": "*"
},
"gitHead": "ee4f30ef3b5fa567ad1bf94794197f7683fdd481"
}

View File

@@ -0,0 +1,10 @@
import { ConfigPlugin } from 'expo/config-plugins';
declare const _default: ConfigPlugin<void | {
locationAlwaysAndWhenInUsePermission?: string | false | undefined;
locationAlwaysPermission?: string | false | undefined;
locationWhenInUsePermission?: string | false | undefined;
isIosBackgroundLocationEnabled?: boolean | undefined;
isAndroidBackgroundLocationEnabled?: boolean | undefined;
isAndroidForegroundServiceEnabled?: boolean | undefined;
}>;
export default _default;

View File

@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const config_plugins_1 = require("expo/config-plugins");
const pkg = require('expo-location/package.json');
const LOCATION_USAGE = 'Allow $(PRODUCT_NAME) to access your location';
const withBackgroundLocation = (config) => {
return (0, config_plugins_1.withInfoPlist)(config, (config) => {
if (!Array.isArray(config.modResults.UIBackgroundModes)) {
config.modResults.UIBackgroundModes = [];
}
if (!config.modResults.UIBackgroundModes.includes('location')) {
config.modResults.UIBackgroundModes.push('location');
}
return config;
});
};
const withLocation = (config, { locationAlwaysAndWhenInUsePermission, locationAlwaysPermission, locationWhenInUsePermission, isIosBackgroundLocationEnabled, isAndroidBackgroundLocationEnabled, isAndroidForegroundServiceEnabled, } = {}) => {
if (isIosBackgroundLocationEnabled) {
config = withBackgroundLocation(config);
}
config_plugins_1.IOSConfig.Permissions.createPermissionsPlugin({
NSLocationAlwaysAndWhenInUseUsageDescription: LOCATION_USAGE,
NSLocationAlwaysUsageDescription: LOCATION_USAGE,
NSLocationWhenInUseUsageDescription: LOCATION_USAGE,
})(config, {
NSLocationAlwaysAndWhenInUseUsageDescription: locationAlwaysAndWhenInUsePermission,
NSLocationAlwaysUsageDescription: locationAlwaysPermission,
NSLocationWhenInUseUsageDescription: locationWhenInUsePermission,
});
// If the user has not specified a value for isAndroidForegroundServiceEnabled,
// we default to the value of isAndroidBackgroundLocationEnabled because we want
// to enable foreground by default if background location is enabled.
const enableAndroidForegroundService = typeof isAndroidForegroundServiceEnabled === 'undefined'
? isAndroidBackgroundLocationEnabled
: isAndroidForegroundServiceEnabled;
return config_plugins_1.AndroidConfig.Permissions.withPermissions(config, [
// Note: these are already added in the library AndroidManifest.xml and so
// are not required here, we may want to remove them in the future.
'android.permission.ACCESS_COARSE_LOCATION',
'android.permission.ACCESS_FINE_LOCATION',
// These permissions are optional, and not listed in the library AndroidManifest.xml
isAndroidBackgroundLocationEnabled && 'android.permission.ACCESS_BACKGROUND_LOCATION',
enableAndroidForegroundService && 'android.permission.FOREGROUND_SERVICE',
enableAndroidForegroundService && 'android.permission.FOREGROUND_SERVICE_LOCATION',
].filter(Boolean));
};
exports.default = (0, config_plugins_1.createRunOncePlugin)(withLocation, pkg.name, pkg.version);

View File

@@ -0,0 +1,81 @@
import {
AndroidConfig,
ConfigPlugin,
IOSConfig,
createRunOncePlugin,
withInfoPlist,
} from 'expo/config-plugins';
const pkg = require('expo-location/package.json');
const LOCATION_USAGE = 'Allow $(PRODUCT_NAME) to access your location';
const withBackgroundLocation: ConfigPlugin = (config) => {
return withInfoPlist(config, (config) => {
if (!Array.isArray(config.modResults.UIBackgroundModes)) {
config.modResults.UIBackgroundModes = [];
}
if (!config.modResults.UIBackgroundModes.includes('location')) {
config.modResults.UIBackgroundModes.push('location');
}
return config;
});
};
const withLocation: ConfigPlugin<
{
locationAlwaysAndWhenInUsePermission?: string | false;
locationAlwaysPermission?: string | false;
locationWhenInUsePermission?: string | false;
isIosBackgroundLocationEnabled?: boolean;
isAndroidBackgroundLocationEnabled?: boolean;
isAndroidForegroundServiceEnabled?: boolean;
} | void
> = (
config,
{
locationAlwaysAndWhenInUsePermission,
locationAlwaysPermission,
locationWhenInUsePermission,
isIosBackgroundLocationEnabled,
isAndroidBackgroundLocationEnabled,
isAndroidForegroundServiceEnabled,
} = {}
) => {
if (isIosBackgroundLocationEnabled) {
config = withBackgroundLocation(config);
}
IOSConfig.Permissions.createPermissionsPlugin({
NSLocationAlwaysAndWhenInUseUsageDescription: LOCATION_USAGE,
NSLocationAlwaysUsageDescription: LOCATION_USAGE,
NSLocationWhenInUseUsageDescription: LOCATION_USAGE,
})(config, {
NSLocationAlwaysAndWhenInUseUsageDescription: locationAlwaysAndWhenInUsePermission,
NSLocationAlwaysUsageDescription: locationAlwaysPermission,
NSLocationWhenInUseUsageDescription: locationWhenInUsePermission,
});
// If the user has not specified a value for isAndroidForegroundServiceEnabled,
// we default to the value of isAndroidBackgroundLocationEnabled because we want
// to enable foreground by default if background location is enabled.
const enableAndroidForegroundService =
typeof isAndroidForegroundServiceEnabled === 'undefined'
? isAndroidBackgroundLocationEnabled
: isAndroidForegroundServiceEnabled;
return AndroidConfig.Permissions.withPermissions(
config,
[
// Note: these are already added in the library AndroidManifest.xml and so
// are not required here, we may want to remove them in the future.
'android.permission.ACCESS_COARSE_LOCATION',
'android.permission.ACCESS_FINE_LOCATION',
// These permissions are optional, and not listed in the library AndroidManifest.xml
isAndroidBackgroundLocationEnabled && 'android.permission.ACCESS_BACKGROUND_LOCATION',
enableAndroidForegroundService && 'android.permission.FOREGROUND_SERVICE',
enableAndroidForegroundService && 'android.permission.FOREGROUND_SERVICE_LOCATION',
].filter(Boolean) as string[]
);
};
export default createRunOncePlugin(withLocation, pkg.name, pkg.version);

View File

@@ -0,0 +1,9 @@
{
"extends": "expo-module-scripts/tsconfig.plugin",
"compilerOptions": {
"outDir": "build",
"rootDir": "src"
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}

View File

@@ -0,0 +1,2 @@
import { requireNativeModule } from 'expo-modules-core';
export default requireNativeModule('ExpoLocation');

View File

@@ -0,0 +1,207 @@
import { PermissionResponse, PermissionStatus, UnavailabilityError } from 'expo-modules-core';
import {
LocationAccuracy,
LocationLastKnownOptions,
LocationObject,
LocationOptions,
} from './Location.types';
import { LocationEventEmitter } from './LocationEventEmitter';
class GeocoderError extends Error {
code: string;
constructor() {
super('Geocoder service is not available for this device.');
this.code = 'E_NO_GEOCODER';
}
}
/**
* Converts `GeolocationPosition` to JavaScript object.
*/
function geolocationPositionToJSON(position: LocationObject): LocationObject {
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: LocationObject, options: LocationLastKnownOptions): boolean {
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): Promise<PermissionResponse> {
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: GeolocationPositionError) => {
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: LocationObject | null = null;
export default {
async getProviderStatusAsync(): Promise<{ locationServicesEnabled: boolean }> {
return {
locationServicesEnabled: 'geolocation' in navigator,
};
},
async getLastKnownPositionAsync(
options: LocationLastKnownOptions = {}
): Promise<LocationObject | null> {
if (lastKnownPosition && isLocationValid(lastKnownPosition, options)) {
return lastKnownPosition;
}
return null;
},
async getCurrentPositionAsync(options: LocationOptions): Promise<LocationObject> {
return new Promise<LocationObject>((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): Promise<void> {
navigator.geolocation.clearWatch(watchId);
},
async watchDeviceHeading(headingId): Promise<void> {
console.warn('Location.watchDeviceHeading: is not supported on web');
},
async hasServicesEnabledAsync(): Promise<boolean> {
return 'geolocation' in navigator;
},
async geocodeAsync(): Promise<any[]> {
throw new GeocoderError();
},
async reverseGeocodeAsync(): Promise<any[]> {
throw new GeocoderError();
},
async watchPositionImplAsync(watchId: string, options: LocationOptions): Promise<string> {
return new Promise<string>((resolve) => {
// @ts-ignore: the types here need to be fixed
watchId = global.navigator.geolocation.watchPosition(
(position) => {
lastKnownPosition = geolocationPositionToJSON(position);
LocationEventEmitter.emit('Expo.locationChanged', {
watchId,
location: lastKnownPosition,
});
},
undefined,
// @ts-ignore: the options object needs to be fixed
options
);
resolve(watchId);
});
},
getPermissionsAsync,
async requestPermissionsAsync(): Promise<PermissionResponse> {
return getPermissionsAsync(true);
},
async requestForegroundPermissionsAsync(): Promise<PermissionResponse> {
return getPermissionsAsync(true);
},
async requestBackgroundPermissionsAsync(): Promise<PermissionResponse> {
return getPermissionsAsync(true);
},
async getForegroundPermissionsAsync(): Promise<PermissionResponse> {
return getPermissionsAsync();
},
async getBackgroundPermissionsAsync(): Promise<PermissionResponse> {
return getPermissionsAsync();
},
// no-op
startObserving() {},
stopObserving() {},
};

View File

@@ -0,0 +1,92 @@
import { Platform } from 'expo-modules-core';
import ExpoLocation from './ExpoLocation';
import { LocationObject, LocationAccuracy, LocationOptions } from './Location.types';
import { LocationSubscriber } from './LocationSubscribers';
type GeolocationSuccessCallback = (data: LocationObject) => void;
type GeolocationErrorCallback = (error: any) => void;
type GeolocationOptions = {
enableHighAccuracy?: boolean;
};
declare const global: any;
// @needsAudit
/**
* Polyfills `navigator.geolocation` for interop with the core React Native and Web API approach to geolocation.
*/
export function installWebGeolocationPolyfill(): void {
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,
// We don't polyfill stopObserving, this is an internal method that probably should not even exist
// in react-native docs
stopObserving: () => {},
};
}
}
function convertGeolocationOptions(options: GeolocationOptions): LocationOptions {
return {
accuracy: options.enableHighAccuracy ? LocationAccuracy.High : LocationAccuracy.Balanced,
};
}
function getCurrentPosition(
success: GeolocationSuccessCallback,
error: GeolocationErrorCallback = () => {},
options: GeolocationOptions = {}
): void {
_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: GeolocationSuccessCallback,
error: GeolocationErrorCallback,
options: GeolocationOptions
): Promise<any> {
try {
await ExpoLocation.requestPermissionsAsync();
const result = await ExpoLocation.getCurrentPositionAsync(convertGeolocationOptions(options));
success(result);
} catch (e) {
error(e);
}
}
// Polyfill: navigator.geolocation.watchPosition
function watchPosition(
success: GeolocationSuccessCallback,
error: GeolocationErrorCallback,
options: GeolocationOptions
) {
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: number) {
LocationSubscriber.unregisterCallback(watchId);
}

View File

@@ -0,0 +1,528 @@
import {
PermissionStatus,
PermissionResponse,
PermissionHookOptions,
createPermissionHook,
Platform,
} from 'expo-modules-core';
import ExpoLocation from './ExpoLocation';
import {
LocationAccuracy,
LocationCallback,
LocationGeocodedAddress,
LocationGeocodedLocation,
LocationHeadingCallback,
LocationHeadingObject,
LocationLastKnownOptions,
LocationObject,
LocationOptions,
LocationPermissionResponse,
LocationProviderStatus,
LocationRegion,
LocationSubscription,
LocationTaskOptions,
LocationActivityType,
LocationGeofencingEventType,
LocationGeofencingRegionState,
LocationGeocodingOptions,
} from './Location.types';
import { LocationEventEmitter } from './LocationEventEmitter';
import { LocationSubscriber, HeadingSubscriber, _getCurrentWatchId } from './LocationSubscribers';
// @needsAudit
/**
* @deprecated The Geocoding web api is no longer available from SDK 49 onwards. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
* @param _apiKey Google API key obtained from Google API Console. This API key must have `Geocoding API`
* enabled, otherwise your geocoding requests will be denied.
*/
function setGoogleApiKey(_apiKey: string) {}
// @needsAudit
/**
* Check status of location providers.
* @return A promise which fulfills with an object of type [LocationProviderStatus](#locationproviderstatus).
*/
export async function getProviderStatusAsync(): Promise<LocationProviderStatus> {
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.
*/
export async function enableNetworkProviderAsync(): Promise<void> {
// 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 [`Location.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: LocationOptions = {}
): Promise<LocationObject> {
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: LocationLastKnownOptions = {}
): Promise<LocationObject | null> {
return ExpoLocation.getLastKnownPositionAsync(options);
}
// @needsAudit
/**
* Subscribe to location updates from the device. Please note that updates will only occur while the
* application is in the foreground. To get location updates while in background you'll need to use
* [Location.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.
* @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
*/
export async function watchPositionAsync(
options: LocationOptions,
callback: LocationCallback
): Promise<LocationSubscription> {
const watchId = LocationSubscriber.registerCallback(callback);
await ExpoLocation.watchPositionImplAsync(watchId, options);
return {
remove() {
LocationSubscriber.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(): Promise<LocationHeadingObject> {
return new Promise(async (resolve) => {
let tries = 0;
const subscription = await watchHeadingAsync((heading) => {
if (heading.accuracy > 1 || tries > 5) {
subscription.remove();
resolve(heading);
} else {
tries += 1;
}
});
});
}
// @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.
* @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
*/
export async function watchHeadingAsync(
callback: LocationHeadingCallback
): Promise<LocationSubscription> {
const watchId = HeadingSubscriber.registerCallback(callback);
await ExpoLocation.watchDeviceHeading(watchId);
return {
remove() {
HeadingSubscriber.unregisterCallback(watchId);
},
};
}
// @needsAudit
/**
* Geocode an address string to latitude-longitude location.
* > **Note**: Using the Geocoding web api is no longer supported. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
*
* > **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.
*
* > On Android, you must request a location permission (`Permissions.LOCATION`) from the user
* > before geocoding can be used.
* @param address A string representing address, eg. `"Baker Street London"`.
* @param options
* @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedLocation`](#locationgeocodedlocation) objects.
*/
export async function geocodeAsync(
address: string,
options?: LocationGeocodingOptions
): Promise<LocationGeocodedLocation[]> {
if (typeof address !== 'string') {
throw new TypeError(`Address to geocode must be a string. Got ${address} instead.`);
}
if (options?.useGoogleMaps || 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.
* > **Note**: Using the Geocoding web api is no longer supported. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
*
* > **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.
*
* > On Android, you must request a location permission (`Permissions.LOCATION`) from the user
* > before geocoding can be used.
* @param location An object representing a location.
* @param options
* @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedAddress`](#locationgeocodedaddress) objects.
*/
export async function reverseGeocodeAsync(
location: Pick<LocationGeocodedLocation, 'latitude' | 'longitude'>,
options?: LocationGeocodingOptions
): Promise<LocationGeocodedAddress[]> {
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 (options?.useGoogleMaps || 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.
* @return A promise that fulfills with an object of type [LocationPermissionResponse](#locationpermissionresponse).
* @deprecated Use [`getForegroundPermissionsAsync`](#locationgetforegroundpermissionsasync) or [`getBackgroundPermissionsAsync`](#locationgetbackgroundpermissionsasync) instead.
*/
export async function getPermissionsAsync(): Promise<LocationPermissionResponse> {
console.warn(
`"getPermissionsAsync()" is now deprecated. Please use "getForegroundPermissionsAsync()" or "getBackgroundPermissionsAsync()" instead.`
);
return await ExpoLocation.getPermissionsAsync();
}
// @needsAudit
/**
* Asks the user to grant permissions for location.
* @return A promise that fulfills with an object of type [LocationPermissionResponse](#locationpermissionresponse).
* @deprecated Use [`requestForegroundPermissionsAsync`](#locationrequestforegroundpermissionsasync) or [`requestBackgroundPermissionsAsync`](#locationrequestbackgroundpermissionsasync) instead.
*/
export async function requestPermissionsAsync(): Promise<LocationPermissionResponse> {
console.warn(
`"requestPermissionsAsync()" is now deprecated. Please use "requestForegroundPermissionsAsync()" or "requestBackgroundPermissionsAsync()" instead.`
);
return await ExpoLocation.requestPermissionsAsync();
}
// @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 [PermissionResponse](#permissionresponse).
*/
export async function getForegroundPermissionsAsync(): Promise<LocationPermissionResponse> {
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 [PermissionResponse](#permissionresponse).
*/
export async function requestForegroundPermissionsAsync(): Promise<LocationPermissionResponse> {
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(): Promise<PermissionResponse> {
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(): Promise<PermissionResponse> {
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(): Promise<boolean> {
return await ExpoLocation.hasServicesEnabledAsync();
}
// --- Background location updates
function _validateTaskName(taskName: string) {
if (!taskName || typeof taskName !== 'string') {
throw new Error(`\`taskName\` must be a non-empty string. Got ${taskName} instead.`);
}
}
// @docsMissing
export async function isBackgroundLocationAvailableAsync(): Promise<boolean> {
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.
*
* ```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: string,
options: LocationTaskOptions = { accuracy: LocationAccuracy.Balanced }
): Promise<void> {
_validateTaskName(taskName);
await ExpoLocation.startLocationUpdatesAsync(taskName, options);
}
// @needsAudit
/**
* Stops geofencing 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: string): Promise<void> {
_validateTaskName(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: string): Promise<boolean> {
_validateTaskName(taskName);
return ExpoLocation.hasStartedLocationUpdatesAsync(taskName);
}
// --- Geofencing
function _validateRegions(regions: LocationRegion[]) {
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: string,
regions: LocationRegion[] = []
): Promise<void> {
_validateTaskName(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: string): Promise<void> {
_validateTaskName(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: string): Promise<boolean> {
_validateTaskName(taskName);
return ExpoLocation.hasStartedGeofencingAsync(taskName);
}
export { LocationEventEmitter as EventEmitter, _getCurrentWatchId };
export {
LocationAccuracy as Accuracy,
LocationActivityType as ActivityType,
LocationGeofencingEventType as GeofencingEventType,
LocationGeofencingRegionState as GeofencingRegionState,
PermissionStatus,
PermissionHookOptions,
setGoogleApiKey,
};
export { installWebGeolocationPolyfill } from './GeolocationPolyfill';
export * from './Location.types';

View File

@@ -0,0 +1,509 @@
import { PermissionResponse } from 'expo-modules-core';
// @needsAudit
/**
* Enum with available location accuracies.
*/
export 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,
}
// @needsAudit
/**
* Enum with available activity types of background location tracking.
*/
export 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 12+
*/
Airborne = 5,
}
// @needsAudit
/**
* A type of the event that geofencing task can receive.
*/
export enum LocationGeofencingEventType {
/**
* Emitted when the device entered observed region.
*/
Enter = 1,
/**
* Occurs as soon as the device left observed region
*/
Exit = 2,
}
// @needsAudit
/**
* State of the geofencing region that you receive through the geofencing task.
*/
export 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,
}
// @needsAudit
/**
* 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).
*/
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;
};
// @needsAudit
/**
* 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;
};
// @needsAudit
/**
* 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 11+
*/
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;
// @docsMissing
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;
};
// @needsAudit
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;
};
// @needsAudit
/**
* 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;
};
// @needsAudit
/**
* 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;
};
// @needsAudit
/**
* 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;
};
// @needsAudit
/**
* Represents `watchPositionAsync` callback.
*/
export type LocationCallback = (location: LocationObject) => any;
// @needsAudit
/**
* 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;
// @docsMissing
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;
};
// @needsAudit
/**
* 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;
};
// @needsAudit
/**
* Represents `watchHeadingAsync` callback.
*/
export type LocationHeadingCallback = (location: LocationHeadingObject) => any;
// @needsAudit
/**
* An object of options for forward and reverse geocoding.
*/
export type LocationGeocodingOptions = {
/**
* Whether to force using Google Maps API instead of the native implementation.
* Used by default only on Web platform. Requires providing an API key by `setGoogleApiKey`.
*/
useGoogleMaps?: boolean;
};
// @needsAudit
/**
* 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;
};
// @needsAudit
/**
* 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;
};
// @needsAudit
/**
* 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;
};
// @needsAudit
export type PermissionDetailsLocationIOS = {
/**
* The scope of granted permission. Indicates when it's possible to use location.
*/
scope: 'whenInUse' | 'always' | 'none';
};
// @needsAudit
export type PermissionDetailsLocationAndroid = {
/**
* @deprecated Use `accuracy` field instead.
*/
scope: 'fine' | 'coarse' | 'none';
/**
* Indicates the type of location provider.
*/
accuracy: 'fine' | 'coarse' | 'none';
};
// @needsAudit
/**
* `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 };

View File

@@ -0,0 +1,5 @@
import { EventEmitter } from 'expo-modules-core';
import ExpoLocation from './ExpoLocation';
export const LocationEventEmitter = new EventEmitter(ExpoLocation);

View File

@@ -0,0 +1,3 @@
import { EventEmitter } from 'expo-modules-core';
export const LocationEventEmitter = new EventEmitter({} as any);

View File

@@ -0,0 +1,89 @@
import { Subscription } from 'expo-modules-core';
import ExpoLocation from './ExpoLocation';
import { LocationCallback, LocationHeadingCallback } from './Location.types';
import { LocationEventEmitter } from './LocationEventEmitter';
type EventObject = {
watchId: number;
[key: string]: any;
};
let nextWatchId = 0;
class Subscriber<CallbackType extends LocationCallback | LocationHeadingCallback> {
private eventName: string;
private eventDataField: string;
private callbacks: { [id: string]: CallbackType } = {};
private eventSubscription: Subscription | null = null;
constructor(eventName: string, eventDataField: string) {
this.eventName = eventName;
this.eventDataField = eventDataField;
}
maybeInitializeSubscription() {
if (this.eventSubscription) {
return;
}
this.eventSubscription = LocationEventEmitter.addListener(
this.eventName,
(event: EventObject) => this.trigger(event)
);
}
/**
* Registers given callback under new id which is then returned.
*/
registerCallback(callback: CallbackType): number {
this.maybeInitializeSubscription();
const id = ++nextWatchId;
this.callbacks[id] = callback;
return id;
}
/**
* Unregisters a callback with given id and revokes the subscription if possible.
*/
unregisterCallback(id: number): void {
// 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: EventObject): void {
const watchId = event.watchId;
const callback = this.callbacks[watchId];
if (callback) {
callback(event[this.eventDataField]);
} else {
ExpoLocation.removeWatchAsync(watchId);
}
}
}
export const LocationSubscriber = new Subscriber<LocationCallback>(
'Expo.locationChanged',
'location'
);
export const HeadingSubscriber = new Subscriber<LocationHeadingCallback>(
'Expo.headingChanged',
'heading'
);
/**
* @private Necessary for some unit tests.
*/
export function _getCurrentWatchId(): number {
return nextWatchId;
}

View File

@@ -0,0 +1,9 @@
// @generated by expo-module-scripts
{
"extends": "expo-module-scripts/tsconfig.base",
"compilerOptions": {
"outDir": "./build"
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}