Building a mobile app for Linux, part 4: GPS/mobile tracking

Another important part of many mobile apps is location tracking – there is, fortunately, there is a built in api for most Linux systems called Geoclue that should work… There is even a Python-geoclue package, but after some digging I found that this package does not work in Python3. In fact it’s hard to find examples or documentation, if you look at the files of the package you can see there are some basic docs:

but they seem very sparse… reading this “Geoclue.geoclue”, we can know that it is imported with:

from Geocode import geocode

Unfortunately this is not working in Python3… as it states in the documentation. So with Python2 discontinued this is not so useful.

Finding the right library for Python3

Searching in Synaptic, a more promising package is gir1.2-geoclue-2.0. If you read the previous post about importing the openstreetmap library, this name may look familiar! In fact if you look at the properties and installed files in Synaptic you’ll see it installs one non-/doc/ file, that is /usr/lib/x86_64-linux-gnu/girepository-1.0/Geoclue-2.0.typelib

Within this directory you will see many other libraries that you can also interact with in Python. These are binary files, but like most you can grab the contents with “strings” command and see what text is within, which in this case seems to reveal the name in the fourth line:

/usr/lib/x86_64-linux-gnu/girepository-1.0$ strings ./Geoclue-2.0.typelib | head
GOBJ
METADATA
Gio-2.0
Geoclue
libgeoclue-2.so.0
GClue
AccuracyLevel
none
country
city

Note that you can include “Geoclue” in a similar manner as OSM library. In fact, let’s explore the api in the Python3 terminal, it appears this exposes the simple Geoclue geolocation api:

>>> from gi.repository import Geoclue
__main__:1: PyGIWarning: Geoclue was imported without specifying a version first. Use gi.require_version('Geoclue', '1.0') before import to ensure that the right version gets loaded.
>>> dir(Geoclue);
['AccuracyLevel', 'Client', 'ClientIface', 'ClientProxy', 'ClientProxyClass', 'ClientProxyPrivate', 'ClientSkeleton', 'ClientSkeletonClass', 'ClientSkeletonPrivate', 'Location', 'LocationIface', 'LocationProxy', 'LocationProxyClass', 'LocationProxyPrivate', 'LocationSkeleton', 'LocationSkeletonClass', 'LocationSkeletonPrivate', 'Manager', 'ManagerIface', 'ManagerProxy', 'ManagerProxyClass', 'ManagerProxyPrivate', 'ManagerSkeleton', 'ManagerSkeletonClass', 'ManagerSkeletonPrivate', 'Simple', 'SimpleClass', 'SimplePrivate', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__file__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__loader__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__package__', '__path__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__spec__', '__str__', '__subclasshook__', '__weakref__', '_namespace', '_version', 'client_interface_info', 'client_override_properties', 'location_interface_info', 'location_override_properties', 'manager_interface_info', 'manager_override_properties']
>>> dir(Geoclue.Simple)
['__class__', '__copy__', '__deepcopy__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__gdoc__', '__ge__', '__getattribute__', '__gpointer__', '__grefcount__', '__gsignals__', '__gt__', '__gtype__', '__hash__', '__info__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_force_floating', '_ref', '_ref_sink', '_unref', '_unsupported_data_method', '_unsupported_method', 'bind_property', 'bind_property_full', 'chain', 'compat_control', 'connect', 'connect_after', 'connect_data', 'connect_object', 'connect_object_after', 'disconnect', 'disconnect_by_func', 'emit', 'emit_stop_by_name', 'find_property', 'force_floating', 'freeze_notify', 'g_type_instance', 'get_client', 'get_data', 'get_location', 'get_properties', 'get_property', 'get_qdata', 'handler_block', 'handler_block_by_func', 'handler_disconnect', 'handler_is_connected', 'handler_unblock', 'handler_unblock_by_func', 'init_async', 'init_finish', 'install_properties', 'install_property', 'interface_find_property', 'interface_install_property', 'interface_list_properties', 'is_floating', 'list_properties', 'new', 'new_finish', 'new_sync', 'newv_async', 'notify', 'notify_by_pspec', 'override_property', 'parent', 'priv', 'props', 'qdata', 'ref', 'ref_count', 'ref_sink', 'replace_data', 'replace_qdata', 'run_dispose', 'set_data', 'set_properties', 'set_property', 'steal_data', 'steal_qdata', 'stop_emission', 'stop_emission_by_name', 'thaw_notify', 'unref', 'watch_closure', 'weak_ref']

Other files in the directory have similar interfaces you can import and use in Python… for example if you wanted to dig in to NetworkManager api, let’s see what that would be:

/usr/lib/x86_64-linux-gnu/girepository-1.0$ strings ./NetworkManager-1.0.typelib | head
GOBJ
METADATA
GObject-2.0|DBusGLib-1.0
NetworkManager
libnm-util.so.2
80211ApFlags
none
privacy
80211ApSecurityFlags
pair_wep40

And in similar manner, “from gi.repository import NetworkManager” makes the NetworkManager object available.

Exploring the Geoclue API for Python

Now let’s see what the Python Geoclue api has for us. The tab key helps to find some commands and their parameters which are similar to gclue_simple_new_sync(), gclue_simple_new() in the c docs.:

>>> Geoclue.Simple.new_sync.__doc__
'new_sync(desktop_id:str, accuracy_level:Geoclue.AccuracyLevel, cancellable:Gio.Cancellable=None) -> Geoclue.Simple'
>>> Geoclue.Simple.new.__doc__
'new(desktop_id:str, accuracy_level:Geoclue.AccuracyLevel, cancellable:Gio.Cancellable=None, callback:Gio.AsyncReadyCallback=None, user_data=None)'

So let’s see if we can get an approximate location using Geoclue and the get_location as in their docs:

>>> c = Geoclue.Simple.new_sync('something',Geoclue.AccuracyLevel.NEIGHBORHOOD,None)
>>> c
<Geoclue.Simple object at 0x7fce98294948 (GClueSimple at 0x2783fa0)>
>>> c.get_location.__doc__
'get_location(self) -> Geoclue.LocationProxy'
>>> loc = c.get_location()
>>> loc.
Display all 134 possibilities? (y or n)
loc.__class__(                      loc.call_with_unix_fd_list(         loc.install_properties(
loc.__copy__(                       loc.call_with_unix_fd_list_finish(  loc.install_property(
loc.__deepcopy__(                   loc.call_with_unix_fd_list_sync(    loc.interface_find_property(
loc.__delattr__(                    loc.chain(                          loc.interface_info(
loc.__dict__                        loc.compat_control(                 loc.interface_install_property(
loc.__dir__(                        loc.connect(                        loc.interface_list_properties(
loc.__doc__                         loc.connect_after(                  loc.is_floating(
loc.__eq__(                         loc.connect_data(                   loc.list_properties(
loc.__format__(                     loc.connect_object(                 loc.new(
loc.__gdoc__                        loc.connect_object_after(           loc.new_finish(
loc.__ge__(                         loc.disconnect(                     loc.new_for_bus(
loc.__getattr__(                    loc.disconnect_by_func(             loc.new_for_bus_finish(
loc.__getattribute__(               loc.do_g_properties_changed(        loc.new_for_bus_sync(
loc.__gpointer__                    loc.do_g_signal(                    loc.new_sync(
loc.__grefcount__                   loc.emit(                           loc.newv(
loc.__gsignals__                    loc.emit_stop_by_name(              loc.newv_async(
loc.__gt__(                         loc.find_property(                  loc.notify(
loc.__gtype__                       loc.force_floating(                 loc.notify_by_pspec(
loc.__hash__(                       loc.freeze_notify(                  loc.override_properties(
loc.__info__                        loc.g_type_instance                 loc.override_property(
loc.__init__(                       loc.get_cached_property(            loc.priv
loc.__le__(                         loc.get_cached_property_names(      loc.props
loc.__lt__(                         loc.get_connection(                 loc.qdata
loc.__module__                      loc.get_data(                       loc.ref(
loc.__ne__(                         loc.get_default_timeout(            loc.ref_count
loc.__new__(                        loc.get_flags(                      loc.ref_sink(
loc.__reduce__(                     loc.get_info(                       loc.replace_data(
loc.__reduce_ex__(                  loc.get_interface_info(             loc.replace_qdata(
loc.__repr__(                       loc.get_interface_name(             loc.run_dispose(
loc.__setattr__(                    loc.get_name(                       loc.set_cached_property(
loc.__sizeof__(                     loc.get_name_owner(                 loc.set_data(
loc.__str__(                        loc.get_object(                     loc.set_default_timeout(
loc.__subclasshook__(               loc.get_object_path(                loc.set_interface_info(
loc.__weakref__                     loc.get_properties(                 loc.set_object(
loc._force_floating(                loc.get_property(                   loc.set_properties(
loc._ref(                           loc.get_qdata(                      loc.set_property(
loc._ref_sink(                      loc.handler_block(                  loc.steal_data(
loc._unref(                         loc.handler_block_by_func(          loc.steal_qdata(
loc._unsupported_data_method(       loc.handler_disconnect(             loc.stop_emission(
loc._unsupported_method(            loc.handler_is_connected(           loc.stop_emission_by_name(
loc.bind_property(                  loc.handler_unblock(                loc.thaw_notify(
loc.bind_property_full(             loc.handler_unblock_by_func(        loc.unref(
loc.call(                           loc.init(                           loc.watch_closure(
loc.call_finish(                    loc.init_async(                     loc.weak_ref(
loc.call_sync(                      loc.init_finish(                
>>> loc.get_data()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3/dist-packages/gi/overrides/GObject.py", line 550, in _unsupported_data_method
    raise RuntimeError('Data access methods are unsupported. '
RuntimeError: Data access methods are unsupported. Use normal Python attributes instead

This is odd, unlike the c api for the object returned by get_location, there is no get_latitude(), get_longitude(). After some guess and trying I found it is “loc.get_property(‘latitude’)”, “loc.get_property(‘longitude’)”, or “loc.props.longitude”, “loc.props.latitude” for the loc location object you get from that. So, putting it all together, to get an approximate location for your laptop or device you can enter the following at the python3 terminal:

from gi.repository import Geoclue
clue = Geoclue.Simple.new_sync('something',Geoclue.AccuracyLevel.NEIGHBORHOOD,None)
location = clue.get_location()
print(location.get_property('latitude'), location.get_property('longitude'))

Not only that, there are other attributes you can get from the location object:

>>> for i in l.props:
...  print(i)
... 
<GParamDouble 'accuracy'>
<GParamDouble 'altitude'>
<GParamString 'description'>
<GParamDouble 'heading'>
<GParamDouble 'latitude'>
<GParamDouble 'longitude'>
<GParamDouble 'speed'>
<GParamObject 'g-connection'>
<GParamEnum 'g-bus-type'>
<GParamString 'g-name'>
<GParamString 'g-name-owner'>
<GParamFlags 'g-flags'>
<GParamString 'g-object-path'>
<GParamString 'g-interface-name'>
<GParamInt 'g-default-timeout'>
<GParamBoxed 'g-interface-info'>

Note that the Geoclue.AccuracyLevel.NEIGHBORHOOD can be EXACT instead, and it can give a more exact location – I think it is using wifi MAC addresses and Mozilla’s database for geolocation? Another thing you might wonder looking at the above code is, what is the first argument “desktop id” supposed to be? Apparently no one on their mailing list knows either unfortunately 🙁 Perhaps it should be just the name of your application.

Building the app GUI

Let’s add a button with the circle/crosshair icon that’s common for the my-location button for gps, and connect this… There are the stock icons for whatever GTK theme on your system but that doesn’t seem to be one of them. So let’s add one using a svg file I quickly made in Inkscape – pretty quickly using rectangle, circle, move layer to bottom, and object align and distribute options:

I change the “home” button because we don’t want it like the example sending us to some spot in europe but rather where you are. Let’s change that button:

GoImg = Gtk.Image.new_from_file('locateme.svg')
home_button.set_image(GoImg)

Yikes, that is too big on the screen. Like the documentation says let’s make a pixmap like before at set dimensions.

        home_button = Gtk.Button()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale('locateme.svg',width=21,height=21,preserve_aspect_ratio=True)
        GoImg = Gtk.Image.new_from_pixbuf(pixbuf)
        home_button.set_image(GoImg)
        home_button.set_tooltip_text('Find my location')
        home_button.connect('clicked', self.home_clicked)

This revised code looks more like a navigation app! Now plugging in the location code this should make it easy to find yourself on the map and map nearby repeaters:

 
     def home_clicked(self, button):
        self.osm.set_center_and_zoom(-44.39, 171.25, 12)
        #self.getlocation() #Freezes up, odd.
        GObject.timeout_add(1, self.getlocation)
        
    def getlocation(self):
        clue = Geoclue.Simple.new_sync('repeaterstart',Geoclue.AccuracyLevel.EXACT,None)
        location = clue.get_location()
        self.osm.set_center_and_zoom(location.get_property('latitude'), location.get_property('longitude'), 12)

I noted something odd, that the code locked up the whole program if it runs on the UI thread – that is, if you call the function directly in a click handler. Since that is blocking that is not a great thing to run without a thread but I thought it would work. Now, with the revised code there is an easy map center to location button 🙂

(See note below if you are running newer Ubuntu/GNOME)

Since all the data is IRLP for now, it should be good to switch between an area you browse and your current location. So let’s add a back button too:

        home_button.connect('clicked', self.home_clicked)
        back_button = Gtk.Button(stock=Gtk.STOCK_GO_BACK)
        back_button.connect('clicked', self.back_clicked)
        
        cache_button = Gtk.Button('Cache')
        cache_button.connect('clicked', self.cache_clicked)

        self.vbox.pack_start(self.osm, True, True, 0)
        hbox = Gtk.HBox(False, 0)
        hbox.pack_start(home_button, False, True, 0)
        hbox.pack_start(back_button, False, True, 0)
        hbox.pack_start(cache_button, False, True, 0)

and function to go back, and save the place you were when you center to current location:

    def back_clicked(self, button):
        self.osm.set_center_and_zoom(self.lastLat, self.lastLon, 12)
        
    def getlocation(self):
        self.lastLat = self.osm.props.latitude
        self.lastLon = self.osm.props.longitude
        clue = Geoclue.Simple.new_sync('repeaterstart',Geoclue.AccuracyLevel.EXACT,None)
        location = clue.get_location()
        self.osm.set_center_and_zoom(location.get_property('latitude'), location.get_property('longitude'), 12)

    def on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip, data=None):

and now you can easily switch between your current location and where you were browsing to IRLP to!

Update: After upgrading to Ubuntu 18.04 or newer you may notice that it fails with a message:

GLib.Error: g-dbus-error-quark: GDBus.Error:org.freedesktop.DBus.Error.AccessDenied: Agent rejected 'repeaterstart' for user '1000'. Please ensure that 'repeaterstart' has installed a valid repeaterstart.desktop file. (9)

You need to have user approve use of location, which you can add an alert with:

    def privacySettingsOpen(self):
        Gdk.threads_enter()
        dlg = Gtk.MessageDialog(self, 
            0,Gtk.MessageType.WARNING,
            Gtk.ButtonsType.OK,
            'Please allow geolocation to use this feature.')
        response = dlg.run()
        dlg.destroy()
        subprocess.Popen(['gnome-control-center','privacy'])
        Gdk.threads_leave()

From the thread I call this with GObject.idle_add(self.privacySettingsOpen) in the try-except case:

        try:
            clue = Geoclue.Simple.new_sync('repeaterstart',Geoclue.AccuracyLevel.EXACT,None)
            location = clue.get_location()
            self.osm.set_center_and_zoom(location.get_property('latitude'), location.get_property('longitude'), 12)
        except GLib.Error as err:
            print(err)
            GObject.idle_add(self.privacySettingsOpen)

In the next post, see how to set a subprocess on a thread.

Leave a Reply

Your email address will not be published. Required fields are marked *

6 × one =