Add initial core API and docs for geolocation.

This commit is contained in:
Russell Keith-Magee 2024-03-22 12:37:36 +08:00
parent a136ec2549
commit e20010d8af
No known key found for this signature in database
GPG key ID: 3D2DAB6A37BB5BC3
7 changed files with 214 additions and 5 deletions

View file

@ -24,6 +24,7 @@ from toga.command import Command, CommandSet
from toga.documents import Document
from toga.handlers import wrapped_handler
from toga.hardware.camera import Camera
from toga.hardware.geolocation import Geolocation
from toga.icons import Icon
from toga.paths import Paths
from toga.platform import get_platform_factory
@ -670,6 +671,18 @@ class App:
"""The commands available in the app."""
return self._commands
@property
def geolocation(self) -> Geolocation:
"""A representation of the device's geolocation service."""
try:
return self._geolocation
except AttributeError:
# Instantiate the geolocation service for this app on first access
# This will raise an exception if the platform doesn't implement
# the Geolocation API.
self._geolocation = Geolocation(self)
return self._geolocation
@property
def paths(self) -> Paths:
"""Paths for platform-appropriate locations on the user's file system.

View file

@ -163,3 +163,7 @@ class AsyncResult(ABC):
__ne__ = __bool__
__gt__ = __bool__
__ge__ = __bool__
class PermissionResult(AsyncResult):
RESULT_TYPE = "permission"

View file

@ -3,17 +3,13 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from toga.constants import FlashMode
from toga.handlers import AsyncResult
from toga.handlers import AsyncResult, PermissionResult
from toga.platform import get_platform_factory
if TYPE_CHECKING:
from toga.app import App
class PermissionResult(AsyncResult):
RESULT_TYPE = "permission"
class PhotoResult(AsyncResult):
RESULT_TYPE = "photo"

View file

@ -0,0 +1,120 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Protocol
import toga
from toga.handlers import AsyncResult, PermissionResult, wrapped_handler
from toga.platform import get_platform_factory
if TYPE_CHECKING:
from toga.app import App
class LocationResult(AsyncResult):
RESULT_TYPE = "location"
class OnLocationChangeHandler(Protocol):
def __call__(
self,
geolocation: Geolocation,
location: toga.LatLng,
altitude: float | None,
**kwargs: Any,
) -> None:
"""A handler that will be invoked when the user's location changes.
:param geolocation: the Geolocation service that generated the update.
:param location: The user's location as (latitude, longitude).
:param altitude: The user's altitude in meters above WGS84 reference ellipsoid.
Returns None if the altitude could not be determined.
:param kwargs: Ensures compatibility with arguments added in future versions.
"""
...
class Geolocation:
def __init__(self, app: App):
self.factory = get_platform_factory()
self._app = app
self._impl = self.factory.Geolocation(self)
self.on_change = None
@property
def app(self) -> App:
"""The app with which the geolocation service is associated"""
return self._app
@property
def has_permission(self) -> bool:
"""Does the app have permission to use geolocation 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 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 compared directly.
:returns: An asynchronous result; when awaited, returns True if the app has
permission to capture the user's a geolocation; 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 on_change(self) -> OnLocationChangeHandler:
"""The handler to invoke when the user's location changes."""
return self._on_change
@on_change.setter
def on_change(self, handler):
self._on_change = wrapped_handler(self, handler)
def start(self):
"""Start monitoring the user's location for changes.
An :any:`on_change` callback will be generated then when the user's location
changes.
"""
self._impl.start()
def stop(self):
"""Stop monitoring the user's location."""
self._impl.stop()
@property
def current_location(self) -> LocationResult:
"""Obtain the user's current location using the geolocation service.
If the platform requires permission to access the geolocation service, and the
user hasn't previously provided that permission, this will cause permission to
be requested.
**This is an asynchronous property**. If you request this property in
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 compared directly.
:returns: An asynchronous result; when awaited, returns the :any:`toga.Image`
captured by the camera, or ``None`` if the photo was cancelled.
"""
location = LocationResult(None)
self._impl.current_location(location)
return location

View file

@ -0,0 +1,74 @@
Geolocation
===========
A sensor that can capture the geographical location of the device.
.. rst-class:: widget-support
.. csv-filter:: Availability (:ref:`Key <api-status-key>`)
:header-rows: 1
:file: ../../data/widgets_by_platform.csv
:included_cols: 4,5,6,7,8,9,10
:exclude: {0: '(?!(Geolocation|Hardware))'}
Usage
-----
The Geolocation services of a device can be accessed using the
:attr:`~toga.App.geolocation` attribute. This attribute exposes an API that allows you to
check if you have have permission to access geolocation services; and if permission exists,
capture the current location of the device, and set a handler to be notified when position
changes occur.
The Camera API is *asynchronous*. This means the methods that have long-running behavior
(such as requesting permissions and requesting a position) must be ``await``-ed, rather
than being invoked directly. This means they must be invoked from inside an asynchronous
handler:
.. code-block:: python
import toga
class MyApp(toga.App):
...
async def determine_location(self, widget, **kwargs):
location = await self.geolocation.current_location
Most platforms will require some form of device permission to access the geolocation
service. To confirm if you have permission to use the geolocation service, you can call
:any:`Geolocation.has_permission`; you can request to permission using
:any:`Geolocation.request_permission()`.
The calls to request permissions *can* be invoked from a synchronous context (i.e., a
non ``async`` method); however, they are non-blocking when used in this way. Invoking a
method like :any:`Geolocation.request_permission()` will start the process of requesting
permission, but will return *immediately*, without waiting for the user's response. This
allows an app to *request* permissions as part of the startup process, prior to using
the geolocation APIs, without blocking the rest of app startup.
Toga will confirm whether the app has been granted permission to use geolocation
services before invoking any geolocation API. If permission has not yet been granted,
the platform *may* request access at the time of the first geolocation request; however,
this is not guaranteed to be the behavior on all platforms.
Notes
-----
* Apps that use a camera must be configured to provide permission to the camera device.
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``.
* 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 geolocation 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``.
Reference
---------
.. autoclass:: toga.hardware.geolocation.Geolocation

View file

@ -4,3 +4,4 @@ Device and Hardware
.. toctree::
camera
geolocation

View file

@ -30,6 +30,7 @@ ScrollContainer,Layout Widget,:class:`~toga.ScrollContainer`,A container that ca
SplitContainer,Layout Widget,:class:`~toga.SplitContainer`,A container that divides an area into two panels with a movable border,|y|,|y|,|y|,,,,
OptionContainer,Layout Widget,:class:`~toga.OptionContainer`,A container that can display multiple labeled tabs of content,|y|,|y|,|y|,|y|,|y|,,
Camera,Hardware,:class:`~toga.hardware.camera.Camera`,A sensor that can capture photos and/or video.,|y|,,,|y|,|y|,,
Geolocation,Hardware,:class:`~toga.hardware.geolocation.Geolocation`,A sensor that can capture the geographical location of the device.,|y|,,,|y|,|y|,,
Screen,Hardware,:class:`~toga.screens.Screen`,A representation of a screen attached to a device.,|y|,|y|,|y|,|y|,|y|,|b|,|b|
App Paths,Resource,:class:`~toga.paths.Paths`,A mechanism for obtaining platform-appropriate filesystem locations for an application.,|y|,|y|,|y|,|y|,|y|,,|b|
Command,Resource,:class:`~toga.Command`,Command,|y|,|y|,|y|,,|y|,,

1 Component Type Component Description macOS GTK Windows iOS Android Web Terminal
30 SplitContainer Layout Widget :class:`~toga.SplitContainer` A container that divides an area into two panels with a movable border |y| |y| |y|
31 OptionContainer Layout Widget :class:`~toga.OptionContainer` A container that can display multiple labeled tabs of content |y| |y| |y| |y| |y|
32 Camera Hardware :class:`~toga.hardware.camera.Camera` A sensor that can capture photos and/or video. |y| |y| |y|
33 Geolocation Hardware :class:`~toga.hardware.geolocation.Geolocation` A sensor that can capture the geographical location of the device. |y| |y| |y|
34 Screen Hardware :class:`~toga.screens.Screen` A representation of a screen attached to a device. |y| |y| |y| |y| |y| |b| |b|
35 App Paths Resource :class:`~toga.paths.Paths` A mechanism for obtaining platform-appropriate filesystem locations for an application. |y| |y| |y| |y| |y| |b|
36 Command Resource :class:`~toga.Command` Command |y| |y| |y| |y|