Skip to content

Location

Usage

The location services of a device can be accessed using the toga.App.location attribute. This attribute exposes an API that allows you to check if you have have permission to access location services; if permission exists, you can capture the current location of the device, and/or set a handler to be notified when position changes occur.

The Location API is asynchronous. This means the methods that have long-running behavior (such as requesting permissions and requesting a location) must be await-ed, rather than being invoked directly. This means they must be invoked from inside an asynchronous handler:

import toga

class MyApp(toga.App):
    ...
    async def determine_location(self, widget, **kwargs):
        location = await self.location.current_location()

All platforms require some form of permission to access the location service. To confirm if you have permission to use the location service while the app is running, you can call Location.has_permission; you can request permission using Location.request_permission().

If you wish to track the location of the user while the app is in the background, you must make a separate request for background location permissions using Location.request_background_permission() . This request must be made after foreground permissions have been requested and confirmed. To confirm if you have permission to use location while the app is in the background, you can call Location.has_background_permission.

Toga will confirm whether the app has been granted permission to use Location services before invoking any location API. If permission has not yet been granted, or if permission has been denied by the user, a [PermissionError][] will be raised.

To continuously track location, add an on_change handler to the location service, then call Location.start_tracking(). The handler will be invoked whenever a new location is obtained:

class MyApp(toga.App):
    ...
    async def location_update(self, location, altitude, **kwargs):
        print(f"You are now at {location}, with altitude {altitude} meters")

    def start_location(self):
        # Install a location handler
        self.location.on_change = self.location_update
        # Start location updates. This assumes permissions have already been
        # requested and granted.
        try:
            self.location.start_tracking()
        except PermissionError:
            print("User has not permitted location tracking.")

If you no longer wish to receive location updates, call Location.stop_tracking().

System requirements

  • Using location services on Linux requires that the user has installed the system packages for GeoClue2, plus the GObject Introspection bindings for GeoClue2. The name of the system package required is distribution dependent:
    • Ubuntu and Debian: gir1.2-geoclue-2.0
    • Fedora: geoclue2-libs
    • Arch/Manjaro: geoclue
    • OpenSUSE Tumbleweed: geoclue2 typelib(geoclue2)
    • FreeBSD: geoclue
  • The GeoClue service must be enabled for Toga GTK location services to work. Some distributions are pre-configured with GeoClue and require no action from users to enable location services. Others, for example, Ubuntu, have special controls for managing location services, which must be turned on before GeoClue will function. Refer to your distribution's documentation on GeoClue and location services for details on how to manage and configure the GeoClue service.

Notes

  • Apps that use location services must be configured to provide permissions to access those services. The permissions required are platform specific:
    • iOS: NSLocationWhenInUseUsageDescription must be defined in the app's Info.plist file. If you want to track location while the app is in the background, you must also define NSLocationAlwaysAndWhenInUseUsageDescription, and add the location and processing values to UIBackgroundModes.
    • macOS: The com.apple.security.personal-information.location entitlement must be enabled, and NSLocationUsageDescription must be defined in the app's Info.plist file.
    • Android: At least one of the permissions android.permission.ACCESS_FINE_LOCATION or android.permission.ACCESS_COARSE_LOCATION must be declared; if only one is declared, this will impact on the precision available in location results. If you want to track location while the app is in the background, you must also define the permission android.permission.ACCESS_BACKGROUND_LOCATION.
  • On macOS and GTK, there is no distinction between "background" permissions and "while-running" permissions for location tracking.
  • On Linux, there are no reliable permission controls for non-sandboxed applications. Sandboxed applications (e.g., Flatpak apps) request location information via the XDG Portal Location API, which has coarse grained permissions allowing users to reliably disallow location access on a per-app basis. However, Linux users should be aware of the limitations of location privacy for non-sandboxed applications. This applies to all Linux applications, not just ones using Toga GTK's Location implementation.
  • On iOS, if the user has provided "allow once" permission for foreground location tracking, requests for background location permission will be rejected.
  • On Android prior to API 34, altitude is reported as the distance above the WGS84 ellipsoid datum, rather than Mean Sea Level altitude.

Reference

Source code in core/src/toga/hardware/location.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
class Location:
    def __init__(self, app: App):
        self.factory = get_factory()
        self._app = app
        self._impl = self.factory.Location(self)

        self.on_change = None

    @property
    def app(self) -> App:
        """The app with which the location service is associated"""
        return self._app

    @property
    def has_permission(self) -> bool:
        """Does the app have permission to use location services?

        If the platform requires the user to explicitly confirm permission, and
        the user has not yet given permission, this will return `False`.
        """
        return self._impl.has_permission()

    def request_permission(self) -> PermissionResult:
        """Request sufficient permissions to capture the user's location.

        If permission has already been granted, this will return without prompting the
        user.

        This method will only grant permission to access location services while the
        app is in the foreground. If you want your application to have permission to
        track location while the app is in the background, you must call this method,
        then make an *additional* permission request for background permissions using
        [`Location.request_background_permission()`][toga.hardware.location.Location.request_background_permission].

        **This is an asynchronous method**. If you invoke this method in synchronous
        context, it will start the process of requesting permissions, but will return
        *immediately*. The return value can be awaited in an asynchronous context, but
        cannot be used directly.

        :returns: An asynchronous result; when awaited, returns True if the app has
            permission to capture the user's location; False otherwise.
        """
        result = PermissionResult(None)

        if has_permission := self.has_permission:
            result.set_result(has_permission)
        else:
            self._impl.request_permission(result)

        return result

    @property
    def has_background_permission(self) -> bool:
        """Does the app have permission to use location services in the background?

        If the platform requires the user to explicitly confirm permission, and the user
        has not yet given permission, this will return `False`.
        """
        return self._impl.has_background_permission()

    def request_background_permission(self) -> PermissionResult:
        """Request sufficient permissions to capture the user's location in the
        background.

        If permission has already been granted, this will return without prompting the
        user.

        Before requesting background permission, you must first request and receive
        foreground location permission using
        [`Location.request_permission`][toga.hardware.location.Location.request_permission].
        If you ask for background permission before receiving foreground location
        permission, a [`PermissionError`][] will be raised.

        **This is an asynchronous method**. If you invoke this method in synchronous
        context, it will start the process of requesting permissions, but will return
        *immediately*. The return value can be awaited in an asynchronous context, but
        cannot be used directly.

        :returns: An asynchronous result; when awaited, returns True if the app has
            permission to capture the user's location while running in the
            background; False otherwise.
        :raises PermissionError: If the app has not already requested and received
            permission to use location services.
        """
        result = PermissionResult(None)
        if not self.has_permission:
            result.set_exception(
                PermissionError(
                    "Cannot ask for background location permission "
                    "before confirming foreground location permission."
                )
            )
        elif has_background_permission := self.has_background_permission:
            result.set_result(has_background_permission)
        else:
            self._impl.request_background_permission(result)

        return result

    @property
    def on_change(self) -> OnLocationChangeHandler:
        """The handler to invoke when an update to the user's location is available."""
        return self._on_change

    @on_change.setter
    def on_change(self, handler: OnLocationChangeHandler) -> None:
        self._on_change = wrapped_handler(self, handler)

    def start_tracking(self) -> None:
        """Start monitoring the user's location for changes.

        An [`on_change`][toga.hardware.location.Location.on_change] callback will be
        generated when the user's location changes.

        :raises PermissionError: If the app has not requested and received permission to
            use location services.
        """
        if self.has_permission:
            self._impl.start_tracking()
        else:
            raise PermissionError(
                "App does not have permission to use location services"
            )

    def stop_tracking(self) -> None:
        """Stop monitoring the user's location.

        :raises PermissionError: If the app has not requested and received permission to
            use location services.
        """
        if self.has_permission:
            self._impl.stop_tracking()
        else:
            raise PermissionError(
                "App does not have permission to use location services"
            )

    def current_location(self) -> LocationResult:
        """Obtain the user's current location using the location service.

        If the app hasn't requested and received permission to use location services, a
        [`PermissionError`][] will be raised.

        **This is an asynchronous method**. If you call this method in a synchronous
        context, it will start the process of requesting the user's location, but will
        return *immediately*. The return value can be awaited in an asynchronous
        context, but cannot be used directly.

        If location tracking is enabled, and an
        [`on_change`][toga.hardware.location.Location.on_change] handler is installed,
        requesting the current location may also cause that handler to be invoked.

        :returns: An asynchronous result; when awaited, returns the current
            [`toga.LatLng`][] of the device.
        :raises PermissionError: If the app has not requested and received permission to
            use location services.
        """
        location = LocationResult(None)
        if self.has_permission:
            self._impl.current_location(location)
        else:
            location.set_exception(
                PermissionError("App does not have permission to use location services")
            )
        return location

app property

The app with which the location service is associated

has_background_permission property

Does the app have permission to use location services in the background?

If the platform requires the user to explicitly confirm permission, and the user has not yet given permission, this will return False.

has_permission property

Does the app have permission to use location services?

If the platform requires the user to explicitly confirm permission, and the user has not yet given permission, this will return False.

on_change property writable

The handler to invoke when an update to the user's location is available.

current_location()

Obtain the user's current location using the location service.

If the app hasn't requested and received permission to use location services, a [PermissionError][] will be raised.

This is an asynchronous method. If you call this method in a synchronous context, it will start the process of requesting the user's location, but will return immediately. The return value can be awaited in an asynchronous context, but cannot be used directly.

If location tracking is enabled, and an on_change handler is installed, requesting the current location may also cause that handler to be invoked.

:returns: An asynchronous result; when awaited, returns the current toga.LatLng of the device. :raises PermissionError: If the app has not requested and received permission to use location services.

Source code in core/src/toga/hardware/location.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def current_location(self) -> LocationResult:
    """Obtain the user's current location using the location service.

    If the app hasn't requested and received permission to use location services, a
    [`PermissionError`][] will be raised.

    **This is an asynchronous method**. If you call this method in a synchronous
    context, it will start the process of requesting the user's location, but will
    return *immediately*. The return value can be awaited in an asynchronous
    context, but cannot be used directly.

    If location tracking is enabled, and an
    [`on_change`][toga.hardware.location.Location.on_change] handler is installed,
    requesting the current location may also cause that handler to be invoked.

    :returns: An asynchronous result; when awaited, returns the current
        [`toga.LatLng`][] of the device.
    :raises PermissionError: If the app has not requested and received permission to
        use location services.
    """
    location = LocationResult(None)
    if self.has_permission:
        self._impl.current_location(location)
    else:
        location.set_exception(
            PermissionError("App does not have permission to use location services")
        )
    return location

request_background_permission()

Request sufficient permissions to capture the user's location in the background.

If permission has already been granted, this will return without prompting the user.

Before requesting background permission, you must first request and receive foreground location permission using Location.request_permission. If you ask for background permission before receiving foreground location permission, a [PermissionError][] will be raised.

This is an asynchronous method. If you invoke this method in synchronous context, it will start the process of requesting permissions, but will return immediately. The return value can be awaited in an asynchronous context, but cannot be used directly.

:returns: An asynchronous result; when awaited, returns True if the app has permission to capture the user's location while running in the background; False otherwise. :raises PermissionError: If the app has not already requested and received permission to use location services.

Source code in core/src/toga/hardware/location.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def request_background_permission(self) -> PermissionResult:
    """Request sufficient permissions to capture the user's location in the
    background.

    If permission has already been granted, this will return without prompting the
    user.

    Before requesting background permission, you must first request and receive
    foreground location permission using
    [`Location.request_permission`][toga.hardware.location.Location.request_permission].
    If you ask for background permission before receiving foreground location
    permission, a [`PermissionError`][] will be raised.

    **This is an asynchronous method**. If you invoke this method in synchronous
    context, it will start the process of requesting permissions, but will return
    *immediately*. The return value can be awaited in an asynchronous context, but
    cannot be used directly.

    :returns: An asynchronous result; when awaited, returns True if the app has
        permission to capture the user's location while running in the
        background; False otherwise.
    :raises PermissionError: If the app has not already requested and received
        permission to use location services.
    """
    result = PermissionResult(None)
    if not self.has_permission:
        result.set_exception(
            PermissionError(
                "Cannot ask for background location permission "
                "before confirming foreground location permission."
            )
        )
    elif has_background_permission := self.has_background_permission:
        result.set_result(has_background_permission)
    else:
        self._impl.request_background_permission(result)

    return result

request_permission()

Request sufficient permissions to capture the user's location.

If permission has already been granted, this will return without prompting the user.

This method will only grant permission to access location services while the app is in the foreground. If you want your application to have permission to track location while the app is in the background, you must call this method, then make an additional permission request for background permissions using Location.request_background_permission().

This is an asynchronous method. If you invoke this method in synchronous context, it will start the process of requesting permissions, but will return immediately. The return value can be awaited in an asynchronous context, but cannot be used directly.

:returns: An asynchronous result; when awaited, returns True if the app has permission to capture the user's location; False otherwise.

Source code in core/src/toga/hardware/location.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def request_permission(self) -> PermissionResult:
    """Request sufficient permissions to capture the user's location.

    If permission has already been granted, this will return without prompting the
    user.

    This method will only grant permission to access location services while the
    app is in the foreground. If you want your application to have permission to
    track location while the app is in the background, you must call this method,
    then make an *additional* permission request for background permissions using
    [`Location.request_background_permission()`][toga.hardware.location.Location.request_background_permission].

    **This is an asynchronous method**. If you invoke this method in synchronous
    context, it will start the process of requesting permissions, but will return
    *immediately*. The return value can be awaited in an asynchronous context, but
    cannot be used directly.

    :returns: An asynchronous result; when awaited, returns True if the app has
        permission to capture the user's location; False otherwise.
    """
    result = PermissionResult(None)

    if has_permission := self.has_permission:
        result.set_result(has_permission)
    else:
        self._impl.request_permission(result)

    return result

start_tracking()

Start monitoring the user's location for changes.

An on_change callback will be generated when the user's location changes.

:raises PermissionError: If the app has not requested and received permission to use location services.

Source code in core/src/toga/hardware/location.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def start_tracking(self) -> None:
    """Start monitoring the user's location for changes.

    An [`on_change`][toga.hardware.location.Location.on_change] callback will be
    generated when the user's location changes.

    :raises PermissionError: If the app has not requested and received permission to
        use location services.
    """
    if self.has_permission:
        self._impl.start_tracking()
    else:
        raise PermissionError(
            "App does not have permission to use location services"
        )

stop_tracking()

Stop monitoring the user's location.

:raises PermissionError: If the app has not requested and received permission to use location services.

Source code in core/src/toga/hardware/location.py
160
161
162
163
164
165
166
167
168
169
170
171
def stop_tracking(self) -> None:
    """Stop monitoring the user's location.

    :raises PermissionError: If the app has not requested and received permission to
        use location services.
    """
    if self.has_permission:
        self._impl.stop_tracking()
    else:
        raise PermissionError(
            "App does not have permission to use location services"
        )

Bases: Protocol

Source code in core/src/toga/hardware/location.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class OnLocationChangeHandler(Protocol):
    def __call__(
        self,
        *,
        service: Location,
        location: toga.LatLng,
        altitude: float | None,
        **kwargs: Any,
    ) -> object:
        """A handler that will be invoked when the user's location changes.

        :param service: the location service that generated the update.
        :param location: The user's location as (latitude, longitude).
        :param altitude: The user's altitude in meters above mean sea level. Returns
            None if the altitude could not be determined.
        :param kwargs: Ensures compatibility with arguments added in future versions.
        """

__call__(*, service, location, altitude, **kwargs)

A handler that will be invoked when the user's location changes.

:param service: the location service that generated the update. :param location: The user's location as (latitude, longitude). :param altitude: The user's altitude in meters above mean sea level. Returns None if the altitude could not be determined. :param kwargs: Ensures compatibility with arguments added in future versions.

Source code in core/src/toga/hardware/location.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def __call__(
    self,
    *,
    service: Location,
    location: toga.LatLng,
    altitude: float | None,
    **kwargs: Any,
) -> object:
    """A handler that will be invoked when the user's location changes.

    :param service: the location service that generated the update.
    :param location: The user's location as (latitude, longitude).
    :param altitude: The user's altitude in meters above mean sea level. Returns
        None if the altitude could not be determined.
    :param kwargs: Ensures compatibility with arguments added in future versions.
    """