xref: /expo/packages/expo-location/src/Location.ts (revision 1aef436a)
1import {
2  PermissionStatus,
3  PermissionResponse,
4  PermissionHookOptions,
5  createPermissionHook,
6  Platform,
7} from 'expo-modules-core';
8
9import ExpoLocation from './ExpoLocation';
10import {
11  LocationAccuracy,
12  LocationCallback,
13  LocationGeocodedAddress,
14  LocationGeocodedLocation,
15  LocationHeadingCallback,
16  LocationHeadingObject,
17  LocationLastKnownOptions,
18  LocationObject,
19  LocationOptions,
20  LocationPermissionResponse,
21  LocationProviderStatus,
22  LocationRegion,
23  LocationSubscription,
24  LocationTaskOptions,
25  LocationActivityType,
26  LocationGeofencingEventType,
27  LocationGeofencingRegionState,
28  LocationGeocodingOptions,
29} from './Location.types';
30import { LocationEventEmitter } from './LocationEventEmitter';
31import { LocationSubscriber, HeadingSubscriber, _getCurrentWatchId } from './LocationSubscribers';
32
33// @needsAudit
34/**
35 * @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.
36 * @param apiKey Google API key obtained from Google API Console. This API key must have `Geocoding API`
37 * enabled, otherwise your geocoding requests will be denied.
38 */
39function setGoogleApiKey(_apiKey: string) {}
40
41// @needsAudit
42/**
43 * Check status of location providers.
44 * @return A promise which fulfills with an object of type [LocationProviderStatus](#locationproviderstatus).
45 */
46export async function getProviderStatusAsync(): Promise<LocationProviderStatus> {
47  return ExpoLocation.getProviderStatusAsync();
48}
49
50// @needsAudit
51/**
52 * Asks the user to turn on high accuracy location mode which enables network provider that uses
53 * Google Play services to improve location accuracy and location-based services.
54 * @return A promise resolving as soon as the user accepts the dialog. Rejects if denied.
55 */
56export async function enableNetworkProviderAsync(): Promise<void> {
57  // If network provider is disabled (user's location mode is set to "Device only"),
58  // Android's location provider may not give you any results. Use this method in order to ask the user
59  // to change the location mode to "High accuracy" which uses Google Play services and enables network provider.
60  // `getCurrentPositionAsync` and `watchPositionAsync` are doing it automatically anyway.
61
62  if (Platform.OS === 'android') {
63    return ExpoLocation.enableNetworkProviderAsync();
64  }
65}
66
67// @needsAudit
68/**
69 * Requests for one-time delivery of the user's current location.
70 * Depending on given `accuracy` option it may take some time to resolve,
71 * especially when you're inside a building.
72 * > __Note:__ Calling it causes the location manager to obtain a location fix which may take several
73 * > seconds. Consider using [`Location.getLastKnownPositionAsync`](#locationgetlastknownpositionasyncoptions)
74 * > if you expect to get a quick response and high accuracy is not required.
75 * @param options
76 * @return A promise which fulfills with an object of type [`LocationObject`](#locationobject).
77 */
78export async function getCurrentPositionAsync(
79  options: LocationOptions = {}
80): Promise<LocationObject> {
81  return ExpoLocation.getCurrentPositionAsync(options);
82}
83
84// @needsAudit
85/**
86 * Gets the last known position of the device or `null` if it's not available or doesn't match given
87 * requirements such as maximum age or required accuracy.
88 * It's considered to be faster than `getCurrentPositionAsync` as it doesn't request for the current
89 * location, but keep in mind the returned location may not be up-to-date.
90 * @param options
91 * @return A promise which fulfills with an object of type [LocationObject](#locationobject) or
92 * `null` if it's not available or doesn't match given requirements such as maximum age or required
93 * accuracy.
94 */
95export async function getLastKnownPositionAsync(
96  options: LocationLastKnownOptions = {}
97): Promise<LocationObject | null> {
98  return ExpoLocation.getLastKnownPositionAsync(options);
99}
100
101// @needsAudit
102/**
103 * Subscribe to location updates from the device. Please note that updates will only occur while the
104 * application is in the foreground. To get location updates while in background you'll need to use
105 * [Location.startLocationUpdatesAsync](#locationstartlocationupdatesasynctaskname-options).
106 * @param options
107 * @param callback This function is called on each location update. It receives an object of type
108 * [`LocationObject`](#locationobject) as the first argument.
109 * @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
110 */
111export async function watchPositionAsync(
112  options: LocationOptions,
113  callback: LocationCallback
114): Promise<LocationSubscription> {
115  const watchId = LocationSubscriber.registerCallback(callback);
116  await ExpoLocation.watchPositionImplAsync(watchId, options);
117
118  return {
119    remove() {
120      LocationSubscriber.unregisterCallback(watchId);
121    },
122  };
123}
124
125// @needsAudit
126/**
127 * Gets the current heading information from the device. To simplify, it calls `watchHeadingAsync`
128 * and waits for a couple of updates, and then returns the one that is accurate enough.
129 * @return A promise which fulfills with an object of type [LocationHeadingObject](#locationheadingobject).
130 */
131export async function getHeadingAsync(): Promise<LocationHeadingObject> {
132  return new Promise(async (resolve) => {
133    let tries = 0;
134
135    const subscription = await watchHeadingAsync((heading) => {
136      if (heading.accuracy > 1 || tries > 5) {
137        subscription.remove();
138        resolve(heading);
139      } else {
140        tries += 1;
141      }
142    });
143  });
144}
145
146// @needsAudit
147/**
148 * Subscribe to compass updates from the device.
149 * @param callback This function is called on each compass update. It receives an object of type
150 * [LocationHeadingObject](#locationheadingobject) as the first argument.
151 * @return A promise which fulfills with a [`LocationSubscription`](#locationsubscription) object.
152 */
153export async function watchHeadingAsync(
154  callback: LocationHeadingCallback
155): Promise<LocationSubscription> {
156  const watchId = HeadingSubscriber.registerCallback(callback);
157  await ExpoLocation.watchDeviceHeading(watchId);
158
159  return {
160    remove() {
161      HeadingSubscriber.unregisterCallback(watchId);
162    },
163  };
164}
165
166// @needsAudit
167/**
168 * Geocode an address string to latitude-longitude location.
169 * > **Note**: Using the Geocoding web api is no longer supported. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
170 *
171 * > **Note**: Geocoding is resource consuming and has to be used reasonably. Creating too many
172 * > requests at a time can result in an error, so they have to be managed properly.
173 * > It's also discouraged to use geocoding while the app is in the background and its results won't
174 * > be shown to the user immediately.
175 *
176 * > On Android, you must request a location permission (`Permissions.LOCATION`) from the user
177 * > before geocoding can be used.
178 * @param address A string representing address, eg. `"Baker Street London"`.
179 * @param options
180 * @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedLocation`](#locationgeocodedlocation) objects.
181 */
182export async function geocodeAsync(
183  address: string,
184  options?: LocationGeocodingOptions
185): Promise<LocationGeocodedLocation[]> {
186  if (typeof address !== 'string') {
187    throw new TypeError(`Address to geocode must be a string. Got ${address} instead.`);
188  }
189
190  if (options?.useGoogleMaps || Platform.OS === 'web') {
191    if (__DEV__) {
192      console.warn(
193        'The Geocoding API has been removed in SDK 49, use Place Autocomplete service instead' +
194          '(https://developers.google.com/maps/documentation/places/web-service/autocomplete)'
195      );
196    }
197    return [];
198  }
199
200  return await ExpoLocation.geocodeAsync(address);
201}
202
203// @needsAudit
204/**
205 * Reverse geocode a location to postal address.
206 * > **Note**: Using the Geocoding web api is no longer supported. Use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete) instead.
207 *
208 * > **Note**: Geocoding is resource consuming and has to be used reasonably. Creating too many
209 * > requests at a time can result in an error, so they have to be managed properly.
210 * > It's also discouraged to use geocoding while the app is in the background and its results won't
211 * > be shown to the user immediately.
212 *
213 * > On Android, you must request a location permission (`Permissions.LOCATION`) from the user
214 * > before geocoding can be used.
215 * @param location An object representing a location.
216 * @param options
217 * @return A promise which fulfills with an array (in most cases its size is 1) of [`LocationGeocodedAddress`](#locationgeocodedaddress) objects.
218 */
219export async function reverseGeocodeAsync(
220  location: Pick<LocationGeocodedLocation, 'latitude' | 'longitude'>,
221  options?: LocationGeocodingOptions
222): Promise<LocationGeocodedAddress[]> {
223  if (typeof location.latitude !== 'number' || typeof location.longitude !== 'number') {
224    throw new TypeError(
225      'Location to reverse-geocode must be an object with number properties `latitude` and `longitude`.'
226    );
227  }
228
229  if (options?.useGoogleMaps || Platform.OS === 'web') {
230    if (__DEV__) {
231      console.warn(
232        'The Geocoding API has been removed in SDK 49, use Place Autocomplete service instead' +
233          '(https://developers.google.com/maps/documentation/places/web-service/autocomplete)'
234      );
235    }
236    return [];
237  }
238
239  return await ExpoLocation.reverseGeocodeAsync(location);
240}
241
242// @needsAudit
243/**
244 * Checks user's permissions for accessing location.
245 * @return A promise that fulfills with an object of type [LocationPermissionResponse](#locationpermissionresponse).
246 * @deprecated Use [`getForegroundPermissionsAsync`](#locationgetforegroundpermissionsasync) or [`getBackgroundPermissionsAsync`](#locationgetbackgroundpermissionsasync) instead.
247 */
248export async function getPermissionsAsync(): Promise<LocationPermissionResponse> {
249  console.warn(
250    `"getPermissionsAsync()" is now deprecated. Please use "getForegroundPermissionsAsync()" or "getBackgroundPermissionsAsync()" instead.`
251  );
252  return await ExpoLocation.getPermissionsAsync();
253}
254
255// @needsAudit
256/**
257 * Asks the user to grant permissions for location.
258 * @return A promise that fulfills with an object of type [LocationPermissionResponse](#locationpermissionresponse).
259 * @deprecated Use [`requestForegroundPermissionsAsync`](#locationrequestforegroundpermissionsasync) or [`requestBackgroundPermissionsAsync`](#locationrequestbackgroundpermissionsasync) instead.
260 */
261export async function requestPermissionsAsync(): Promise<LocationPermissionResponse> {
262  console.warn(
263    `"requestPermissionsAsync()" is now deprecated. Please use "requestForegroundPermissionsAsync()" or "requestBackgroundPermissionsAsync()" instead.`
264  );
265
266  return await ExpoLocation.requestPermissionsAsync();
267}
268
269// @needsAudit
270/**
271 * Checks user's permissions for accessing location while the app is in the foreground.
272 * @return A promise that fulfills with an object of type [PermissionResponse](#permissionresponse).
273 */
274export async function getForegroundPermissionsAsync(): Promise<LocationPermissionResponse> {
275  return await ExpoLocation.getForegroundPermissionsAsync();
276}
277
278// @needsAudit
279/**
280 * Asks the user to grant permissions for location while the app is in the foreground.
281 * @return A promise that fulfills with an object of type [PermissionResponse](#permissionresponse).
282 */
283export async function requestForegroundPermissionsAsync(): Promise<LocationPermissionResponse> {
284  return await ExpoLocation.requestForegroundPermissionsAsync();
285}
286
287// @needsAudit
288/**
289 * Check or request permissions for the foreground location.
290 * This uses both `requestForegroundPermissionsAsync` and `getForegroundPermissionsAsync` to interact with the permissions.
291 *
292 * @example
293 * ```ts
294 * const [status, requestPermission] = Location.useForegroundPermissions();
295 * ```
296 */
297export const useForegroundPermissions = createPermissionHook({
298  getMethod: getForegroundPermissionsAsync,
299  requestMethod: requestForegroundPermissionsAsync,
300});
301
302// @needsAudit
303/**
304 * Checks user's permissions for accessing location while the app is in the background.
305 * @return A promise that fulfills with an object of type [PermissionResponse](#permissionresponse).
306 */
307export async function getBackgroundPermissionsAsync(): Promise<PermissionResponse> {
308  return await ExpoLocation.getBackgroundPermissionsAsync();
309}
310
311// @needsAudit
312/**
313 * Asks the user to grant permissions for location while the app is in the background.
314 * On __Android 11 or higher__: this method will open the system settings page - before that happens
315 * you should explain to the user why your application needs background location permission.
316 * For example, you can use `Modal` component from `react-native` to do that.
317 * > __Note__: Foreground permissions should be granted before asking for the background permissions
318 * (your app can't obtain background permission without foreground permission).
319 * @return A promise that fulfills with an object of type [PermissionResponse](#permissionresponse).
320 */
321export async function requestBackgroundPermissionsAsync(): Promise<PermissionResponse> {
322  return await ExpoLocation.requestBackgroundPermissionsAsync();
323}
324
325// @needsAudit
326/**
327 * Check or request permissions for the background location.
328 * This uses both `requestBackgroundPermissionsAsync` and `getBackgroundPermissionsAsync` to
329 * interact with the permissions.
330 *
331 * @example
332 * ```ts
333 * const [status, requestPermission] = Location.useBackgroundPermissions();
334 * ```
335 */
336export const useBackgroundPermissions = createPermissionHook({
337  getMethod: getBackgroundPermissionsAsync,
338  requestMethod: requestBackgroundPermissionsAsync,
339});
340
341// --- Location service
342
343// @needsAudit
344/**
345 * Checks whether location services are enabled by the user.
346 * @return A promise which fulfills to `true` if location services are enabled on the device,
347 * or `false` if not.
348 */
349export async function hasServicesEnabledAsync(): Promise<boolean> {
350  return await ExpoLocation.hasServicesEnabledAsync();
351}
352
353// --- Background location updates
354
355function _validateTaskName(taskName: string) {
356  if (!taskName || typeof taskName !== 'string') {
357    throw new Error(`\`taskName\` must be a non-empty string. Got ${taskName} instead.`);
358  }
359}
360
361// @docsMissing
362export async function isBackgroundLocationAvailableAsync(): Promise<boolean> {
363  const providerStatus = await getProviderStatusAsync();
364  return providerStatus.backgroundModeEnabled;
365}
366
367// @needsAudit
368/**
369 * Registers for receiving location updates that can also come when the app is in the background.
370 *
371 * # Task parameters
372 *
373 * Background location task will be receiving following data:
374 * - `locations` - An array of the new locations.
375 *
376 * ```ts
377 * import * as TaskManager from 'expo-task-manager';
378 *
379 * TaskManager.defineTask(YOUR_TASK_NAME, ({ data: { locations }, error }) => {
380 *  if (error) {
381 *    // check `error.message` for more details.
382 *    return;
383 *  }
384 *  console.log('Received new locations', locations);
385 * });
386 * ```
387 *
388 * @param taskName Name of the task receiving location updates.
389 * @param options An object of options passed to the location manager.
390 *
391 * @return A promise resolving once the task with location updates is registered.
392 */
393export async function startLocationUpdatesAsync(
394  taskName: string,
395  options: LocationTaskOptions = { accuracy: LocationAccuracy.Balanced }
396): Promise<void> {
397  _validateTaskName(taskName);
398  await ExpoLocation.startLocationUpdatesAsync(taskName, options);
399}
400
401// @needsAudit
402/**
403 * Stops geofencing for specified task.
404 * @param taskName Name of the background location task to stop.
405 * @return A promise resolving as soon as the task is unregistered.
406 */
407export async function stopLocationUpdatesAsync(taskName: string): Promise<void> {
408  _validateTaskName(taskName);
409  await ExpoLocation.stopLocationUpdatesAsync(taskName);
410}
411
412// @needsAudit
413/**
414 * @param taskName Name of the location task to check.
415 * @return A promise which fulfills with boolean value indicating whether the location task is
416 * started or not.
417 */
418export async function hasStartedLocationUpdatesAsync(taskName: string): Promise<boolean> {
419  _validateTaskName(taskName);
420  return ExpoLocation.hasStartedLocationUpdatesAsync(taskName);
421}
422
423// --- Geofencing
424
425function _validateRegions(regions: LocationRegion[]) {
426  if (!regions || regions.length === 0) {
427    throw new Error(
428      'Regions array cannot be empty. Use `stopGeofencingAsync` if you want to stop geofencing all regions'
429    );
430  }
431  for (const region of regions) {
432    if (typeof region.latitude !== 'number') {
433      throw new TypeError(`Region's latitude must be a number. Got '${region.latitude}' instead.`);
434    }
435    if (typeof region.longitude !== 'number') {
436      throw new TypeError(
437        `Region's longitude must be a number. Got '${region.longitude}' instead.`
438      );
439    }
440    if (typeof region.radius !== 'number') {
441      throw new TypeError(`Region's radius must be a number. Got '${region.radius}' instead.`);
442    }
443  }
444}
445
446// @needsAudit
447/**
448 * Starts geofencing for given regions. When the new event comes, the task with specified name will
449 * be called with the region that the device enter to or exit from.
450 * If you want to add or remove regions from already running geofencing task, you can just call
451 * `startGeofencingAsync` again with the new array of regions.
452 *
453 * # Task parameters
454 *
455 * Geofencing task will be receiving following data:
456 *  - `eventType` - Indicates the reason for calling the task, which can be triggered by entering or exiting the region.
457 *    See [GeofencingEventType](#geofencingeventtype).
458 *  - `region` - Object containing details about updated region. See [LocationRegion](#locationregion) for more details.
459 *
460 * @param taskName Name of the task that will be called when the device enters or exits from specified regions.
461 * @param regions Array of region objects to be geofenced.
462 *
463 * @return A promise resolving as soon as the task is registered.
464 *
465 * @example
466 * ```ts
467 * import { GeofencingEventType } from 'expo-location';
468 * import * as TaskManager from 'expo-task-manager';
469 *
470 *  TaskManager.defineTask(YOUR_TASK_NAME, ({ data: { eventType, region }, error }) => {
471 *   if (error) {
472 *     // check `error.message` for more details.
473 *     return;
474 *   }
475 *   if (eventType === GeofencingEventType.Enter) {
476 *     console.log("You've entered region:", region);
477 *   } else if (eventType === GeofencingEventType.Exit) {
478 *     console.log("You've left region:", region);
479 *   }
480 * });
481 * ```
482 */
483export async function startGeofencingAsync(
484  taskName: string,
485  regions: LocationRegion[] = []
486): Promise<void> {
487  _validateTaskName(taskName);
488  _validateRegions(regions);
489  await ExpoLocation.startGeofencingAsync(taskName, { regions });
490}
491
492// @needsAudit
493/**
494 * Stops geofencing for specified task. It unregisters the background task so the app will not be
495 * receiving any updates, especially in the background.
496 * @param taskName Name of the task to unregister.
497 * @return A promise resolving as soon as the task is unregistered.
498 */
499export async function stopGeofencingAsync(taskName: string): Promise<void> {
500  _validateTaskName(taskName);
501  await ExpoLocation.stopGeofencingAsync(taskName);
502}
503
504// @needsAudit
505/**
506 * @param taskName Name of the geofencing task to check.
507 * @return A promise which fulfills with boolean value indicating whether the geofencing task is
508 * started or not.
509 */
510export async function hasStartedGeofencingAsync(taskName: string): Promise<boolean> {
511  _validateTaskName(taskName);
512  return ExpoLocation.hasStartedGeofencingAsync(taskName);
513}
514
515export { LocationEventEmitter as EventEmitter, _getCurrentWatchId };
516
517export {
518  LocationAccuracy as Accuracy,
519  LocationActivityType as ActivityType,
520  LocationGeofencingEventType as GeofencingEventType,
521  LocationGeofencingRegionState as GeofencingRegionState,
522  PermissionStatus,
523  PermissionHookOptions,
524  setGoogleApiKey,
525};
526
527export { installWebGeolocationPolyfill } from './GeolocationPolyfill';
528export * from './Location.types';
529