// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const GLib = imports.gi.GLib; const Gio = imports.gi.Gio; const Lang = imports.lang; const Shell = imports.gi.Shell; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; const LOCATION_SCHEMA = 'org.gnome.shell.location'; const MAX_ACCURACY_LEVEL = 'max-accuracy-level'; var GeoclueIface = ' \ \ \ \ \ \ \ \ '; const GeoclueManager = Gio.DBusProxy.makeProxyWrapper(GeoclueIface); var AgentIface = ' \ \ \ \ \ \ \ \ \ \ '; const AccuracyLevel = { NONE: 0, COUNTRY: 1, CITY: 4, STREET: 6, EXACT: 8, }; const Indicator = new Lang.Class({ Name: 'LocationIndicator', Extends: PanelMenu.SystemIndicator, _init: function() { this.parent(); this._settings = new Gio.Settings({ schema: LOCATION_SCHEMA }); this._settings.connect('changed::' + MAX_ACCURACY_LEVEL, Lang.bind(this, this._onMaxAccuracyLevelChanged)); this._indicator = this._addIndicator(); this._indicator.icon_name = 'find-location-symbolic'; this._item = new PopupMenu.PopupSubMenuMenuItem(_("Location"), true); this._item.icon.icon_name = 'find-location-symbolic'; var credentials = new Gio.Credentials(); var uid = credentials.get_unix_user(); this._agent = Gio.DBusExportedObject.wrapJSObject(AgentIface, this); this._agent.export(Gio.DBus.system, '/org/freedesktop/GeoClue2/Agent/' + uid); this._item.status.text = _("On"); this._onOffAction = this._item.menu.addAction(_("Turn Off"), Lang.bind(this, this._onOnOffAction)); this._accurateItem = new PopupMenu.PopupMenuItem(_("Accurate (GPS + Network)"), false); this._accurateItem.connect('activate', Lang.bind(this, this._onAccurateItemActivated)); this._item.menu.addMenuItem(this._accurateItem); this._powerSavingItem = new PopupMenu.PopupMenuItem(_("Power Saving (Network Only)"), false); this._powerSavingItem.connect('activate', Lang.bind(this, this._onPowerSavingItemActivated)); this._item.menu.addMenuItem(this._powerSavingItem); this._offItem = new PopupMenu.PopupMenuItem(_("Off"), false); this._offItem.connect('activate', Lang.bind(this, this._onOffItemActivated)); this._item.menu.addMenuItem(this._offItem); this.menu.addMenuItem(this._item); this._watchId = Gio.bus_watch_name(Gio.BusType.SYSTEM, 'org.freedesktop.GeoClue2', 0, Lang.bind(this, this._connectToGeoclue), Lang.bind(this, this._onGeoclueVanished)); this._onMaxAccuracyLevelChanged(); this._connectToGeoclue(); }, get MaxAccuracyLevel() { return this._getMaxAccuracyLevel(); }, // We (and geoclue) have currently no way to reliably identifying apps so // for now, lets just authorize all apps as long as they provide a valid // desktop ID. We also ensure they don't get more accuracy than global max. AuthorizeApp: function(desktop_id, reqAccuracyLevel) { var appSystem = Shell.AppSystem.get_default(); var app = appSystem.lookup_app(desktop_id + ".desktop"); if (app == null) { return [false, 0]; } let allowedAccuracyLevel = clamp(reqAccuracyLevel, 0, this._getMaxAccuracyLevel()); return [true, allowedAccuracyLevel]; }, _syncIndicator: function() { if (this._proxy == null) { this._indicator.visible = false; return; } this._indicator.visible = this._proxy.InUse; }, _connectToGeoclue: function() { if (this._proxy != null || this._connecting) return false; this._connecting = true; new GeoclueManager(Gio.DBus.system, 'org.freedesktop.GeoClue2', '/org/freedesktop/GeoClue2/Manager', Lang.bind(this, this._onProxyReady)); return true; }, _onProxyReady: function(proxy, error) { if (error != null) { log(error.message); this._connecting = false; return; } this._proxy = proxy; this._propertiesChangedId = this._proxy.connect('g-properties-changed', Lang.bind(this, this._onGeocluePropsChanged)); this._updateMenu(); this._syncIndicator(); this._proxy.AddAgentRemote('gnome-shell', Lang.bind(this, this._onAgentRegistered)); }, _onAgentRegistered: function(result, error) { this._connecting = false; this._notifyMaxAccuracyLevel(); if (error != null) log(error.message); }, _onGeoclueVanished: function() { if (this._propertiesChangedId) { this._proxy.disconnect(this._propertiesChangedId); this._propertiesChangedId = 0; } this._proxy = null; this._syncIndicator(); }, _onOnOffAction: function() { if (this._getMaxAccuracyLevel() == 0) this._settings.set_enum(MAX_ACCURACY_LEVEL, this._availableAccuracyLevel); else this._settings.set_enum(MAX_ACCURACY_LEVEL, AccuracyLevel.NONE); }, _onAccurateItemActivated: function() { this._settings.set_enum(MAX_ACCURACY_LEVEL, AccuracyLevel.EXACT); }, _onPowerSavingItemActivated: function() { this._settings.set_enum(MAX_ACCURACY_LEVEL, AccuracyLevel.STREET); }, _onOffItemActivated: function() { this._settings.set_enum(MAX_ACCURACY_LEVEL, AccuracyLevel.NONE); }, _onMaxAccuracyLevelChanged: function() { let maxAccuracyLevel = this._getMaxAccuracyLevel(); if (this._availableAccuracyLevel < AccuracyLevel.EXACT) { if (maxAccuracyLevel == 0) { this._item.status.text = _("Off"); this._onOffAction.label.text = _("Turn On"); } else { this._item.status.text = _("On"); this._onOffAction.label.text = _("Turn Off"); } } else { if (maxAccuracyLevel == 0) { this._item.status.text = _("Off"); this._offItem.setOrnament(PopupMenu.Ornament.DOT); this._accurateItem.setOrnament(PopupMenu.Ornament.NONE); this._powerSavingItem.setOrnament(PopupMenu.Ornament.NONE); } else if (maxAccuracyLevel < AccuracyLevel.EXACT) { this._item.status.text = _("Power Saving"); this._powerSavingItem.setOrnament(PopupMenu.Ornament.DOT); this._accurateItem.setOrnament(PopupMenu.Ornament.NONE); this._offItem.setOrnament(PopupMenu.Ornament.NONE); } else { this._item.status.text = _("Accurate"); this._accurateItem.setOrnament(PopupMenu.Ornament.DOT); this._offItem.setOrnament(PopupMenu.Ornament.NONE); this._powerSavingItem.setOrnament(PopupMenu.Ornament.NONE); } } // Gotta ensure geoclue is up and we are registered as agent to it // before we emit the notify for this property change. if (!this._connectToGeoclue()) this._notifyMaxAccuracyLevel(); }, _getMaxAccuracyLevel: function() { return this._settings.get_enum(MAX_ACCURACY_LEVEL); }, _notifyMaxAccuracyLevel: function() { let variant = new GLib.Variant('u', this._getMaxAccuracyLevel()); this._agent.emit_property_changed('MaxAccuracyLevel', variant); }, _updateMenu: function() { this._availableAccuracyLevel = this._proxy.AvailableAccuracyLevel; this.menu.actor.visible = (this._availableAccuracyLevel != 0); if (this._availableAccuracyLevel == 0) return; if (this._availableAccuracyLevel < AccuracyLevel.EXACT) { this._onOffAction.actor.show(); this._accurateItem.actor.hide(); this._powerSavingItem.actor.hide(); this._offItem.actor.hide(); } else { this._onOffAction.actor.hide(); this._accurateItem.actor.show(); this._powerSavingItem.actor.show(); this._offItem.actor.show(); } this._onMaxAccuracyLevelChanged(); }, _onGeocluePropsChanged: function(proxy, properties) { let unpacked = properties.deep_unpack(); if ("InUse" in unpacked) this._syncIndicator(); if ("AvailableAccuracyLevel" in unpacked) this._updateMenu(); } }); function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }