ae86b58d59
Prior to Geoclue v2.7, the location description did not seem to ever be set. Now, the source used to determine the location is set as the description. This can result in the location name in dateMenu to be set as "WiFi", "GeoIP", etc., since we relay the description as the location name to GWeather. Instead, pass an empty string so GWeather continues to set the location name itself. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2729>
330 lines
10 KiB
JavaScript
330 lines
10 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
import Geoclue from 'gi://Geoclue';
|
|
import Gio from 'gi://Gio';
|
|
import GLib from 'gi://GLib';
|
|
import GWeather from 'gi://GWeather';
|
|
import Shell from 'gi://Shell';
|
|
import * as Signals from './signals.js';
|
|
|
|
import * as PermissionStore from './permissionStore.js';
|
|
|
|
import {loadInterfaceXML} from './fileUtils.js';
|
|
|
|
Gio._promisify(Geoclue.Simple, 'new');
|
|
|
|
const WeatherIntegrationIface = loadInterfaceXML('org.gnome.Shell.WeatherIntegration');
|
|
|
|
const WEATHER_BUS_NAME = 'org.gnome.Weather';
|
|
const WEATHER_OBJECT_PATH = '/org/gnome/Weather';
|
|
const WEATHER_INTEGRATION_IFACE = 'org.gnome.Shell.WeatherIntegration';
|
|
|
|
const WEATHER_APP_ID = 'org.gnome.Weather.desktop';
|
|
|
|
// Minimum time between updates to show loading indication
|
|
const UPDATE_THRESHOLD = 10 * GLib.TIME_SPAN_MINUTE;
|
|
|
|
export class WeatherClient extends Signals.EventEmitter {
|
|
constructor() {
|
|
super();
|
|
|
|
this._loading = false;
|
|
this._locationValid = false;
|
|
this._lastUpdate = GLib.DateTime.new_from_unix_local(0);
|
|
|
|
this._autoLocationRequested = false;
|
|
this._mostRecentLocation = null;
|
|
|
|
this._gclueService = null;
|
|
this._gclueStarted = false;
|
|
this._gclueStarting = false;
|
|
this._gclueLocationChangedId = 0;
|
|
|
|
this._needsAuth = true;
|
|
this._weatherAuthorized = false;
|
|
this._permStore = new PermissionStore.PermissionStore(async (proxy, error) => {
|
|
if (error) {
|
|
log(`Failed to connect to permissionStore: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
if (this._permStore.g_name_owner == null) {
|
|
// Failed to auto-start, likely because xdg-desktop-portal
|
|
// isn't installed; don't restrict access to location service
|
|
this._weatherAuthorized = true;
|
|
this._updateAutoLocation();
|
|
return;
|
|
}
|
|
|
|
let [perms, data] = [{}, null];
|
|
try {
|
|
[perms, data] = await this._permStore.LookupAsync('gnome', 'geolocation');
|
|
} catch (err) {
|
|
log(`Error looking up permission: ${err.message}`);
|
|
}
|
|
|
|
const params = ['gnome', 'geolocation', false, data, perms];
|
|
this._onPermStoreChanged(this._permStore, '', params);
|
|
});
|
|
this._permStore.connectSignal('Changed',
|
|
this._onPermStoreChanged.bind(this));
|
|
|
|
this._locationSettings = new Gio.Settings({schema_id: 'org.gnome.system.location'});
|
|
this._locationSettings.connect('changed::enabled',
|
|
this._updateAutoLocation.bind(this));
|
|
|
|
this._world = GWeather.Location.get_world();
|
|
|
|
const providers =
|
|
GWeather.Provider.METAR |
|
|
GWeather.Provider.MET_NO |
|
|
GWeather.Provider.OWM;
|
|
this._weatherInfo = new GWeather.Info({
|
|
application_id: 'org.gnome.Shell',
|
|
contact_info: 'https://gitlab.gnome.org/GNOME/gnome-shell/-/raw/HEAD/gnome-shell.doap',
|
|
enabled_providers: providers,
|
|
});
|
|
this._weatherInfo.connect_after('updated', () => {
|
|
this._lastUpdate = GLib.DateTime.new_now_local();
|
|
this.emit('changed');
|
|
});
|
|
|
|
this._weatherApp = null;
|
|
this._weatherProxy = null;
|
|
|
|
this._createWeatherProxy();
|
|
|
|
this._settings = new Gio.Settings({
|
|
schema_id: 'org.gnome.shell.weather',
|
|
});
|
|
this._settings.connect('changed::automatic-location',
|
|
this._onAutomaticLocationChanged.bind(this));
|
|
this._onAutomaticLocationChanged();
|
|
this._settings.connect('changed::locations',
|
|
this._onLocationsChanged.bind(this));
|
|
this._onLocationsChanged();
|
|
|
|
this._appSystem = Shell.AppSystem.get_default();
|
|
this._appSystem.connect('installed-changed',
|
|
this._onInstalledChanged.bind(this));
|
|
this._onInstalledChanged();
|
|
}
|
|
|
|
get available() {
|
|
return this._weatherApp != null;
|
|
}
|
|
|
|
get loading() {
|
|
return this._loading;
|
|
}
|
|
|
|
get hasLocation() {
|
|
return this._locationValid;
|
|
}
|
|
|
|
get info() {
|
|
return this._weatherInfo;
|
|
}
|
|
|
|
activateApp() {
|
|
if (this._weatherApp)
|
|
this._weatherApp.activate();
|
|
}
|
|
|
|
update() {
|
|
if (!this._locationValid)
|
|
return;
|
|
|
|
let now = GLib.DateTime.new_now_local();
|
|
// Update without loading indication if the current info is recent enough
|
|
if (this._weatherInfo.is_valid() &&
|
|
now.difference(this._lastUpdate) < UPDATE_THRESHOLD)
|
|
this._weatherInfo.update();
|
|
else
|
|
this._loadInfo();
|
|
}
|
|
|
|
get _useAutoLocation() {
|
|
return this._autoLocationRequested &&
|
|
this._locationSettings.get_boolean('enabled') &&
|
|
(!this._needsAuth || this._weatherAuthorized);
|
|
}
|
|
|
|
async _createWeatherProxy() {
|
|
const nodeInfo = Gio.DBusNodeInfo.new_for_xml(WeatherIntegrationIface);
|
|
try {
|
|
this._weatherProxy = await Gio.DBusProxy.new(
|
|
Gio.DBus.session,
|
|
Gio.DBusProxyFlags.DO_NOT_AUTO_START | Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES,
|
|
nodeInfo.lookup_interface(WEATHER_INTEGRATION_IFACE),
|
|
WEATHER_BUS_NAME,
|
|
WEATHER_OBJECT_PATH,
|
|
WEATHER_INTEGRATION_IFACE,
|
|
null);
|
|
} catch (e) {
|
|
log(`Failed to create GNOME Weather proxy: ${e}`);
|
|
return;
|
|
}
|
|
|
|
this._weatherProxy.connect('g-properties-changed',
|
|
this._onWeatherPropertiesChanged.bind(this));
|
|
this._onWeatherPropertiesChanged();
|
|
}
|
|
|
|
_onWeatherPropertiesChanged() {
|
|
if (this._weatherProxy.g_name_owner == null)
|
|
return;
|
|
|
|
this._settings.set_boolean('automatic-location',
|
|
this._weatherProxy.AutomaticLocation);
|
|
this._settings.set_value('locations',
|
|
new GLib.Variant('av', this._weatherProxy.Locations));
|
|
}
|
|
|
|
_onInstalledChanged() {
|
|
let hadApp = this._weatherApp != null;
|
|
this._weatherApp = this._appSystem.lookup_app(WEATHER_APP_ID);
|
|
let haveApp = this._weatherApp != null;
|
|
|
|
if (hadApp !== haveApp)
|
|
this.emit('changed');
|
|
|
|
let neededAuth = this._needsAuth;
|
|
this._needsAuth = this._weatherApp === null ||
|
|
this._weatherApp.app_info.has_key('X-Flatpak');
|
|
|
|
if (neededAuth !== this._needsAuth)
|
|
this._updateAutoLocation();
|
|
}
|
|
|
|
_loadInfo() {
|
|
let id = this._weatherInfo.connect('updated', () => {
|
|
this._weatherInfo.disconnect(id);
|
|
this._loading = false;
|
|
});
|
|
|
|
this._loading = true;
|
|
this.emit('changed');
|
|
|
|
this._weatherInfo.update();
|
|
}
|
|
|
|
_locationsEqual(loc1, loc2) {
|
|
if (loc1 === loc2)
|
|
return true;
|
|
|
|
if (loc1 == null || loc2 == null)
|
|
return false;
|
|
|
|
return loc1.equal(loc2);
|
|
}
|
|
|
|
_setLocation(location) {
|
|
if (this._locationsEqual(this._weatherInfo.location, location))
|
|
return;
|
|
|
|
this._weatherInfo.abort();
|
|
this._weatherInfo.set_location(location);
|
|
this._locationValid = location != null;
|
|
|
|
if (location)
|
|
this._loadInfo();
|
|
else
|
|
this.emit('changed');
|
|
}
|
|
|
|
_updateLocationMonitoring() {
|
|
if (this._useAutoLocation) {
|
|
if (this._gclueLocationChangedId !== 0 || this._gclueService == null)
|
|
return;
|
|
|
|
this._gclueLocationChangedId =
|
|
this._gclueService.connect('notify::location',
|
|
this._onGClueLocationChanged.bind(this));
|
|
this._onGClueLocationChanged();
|
|
} else {
|
|
if (this._gclueLocationChangedId)
|
|
this._gclueService.disconnect(this._gclueLocationChangedId);
|
|
this._gclueLocationChangedId = 0;
|
|
}
|
|
}
|
|
|
|
async _startGClueService() {
|
|
if (this._gclueStarting)
|
|
return;
|
|
|
|
this._gclueStarting = true;
|
|
|
|
try {
|
|
this._gclueService = await Geoclue.Simple.new(
|
|
'org.gnome.Shell', Geoclue.AccuracyLevel.CITY, null);
|
|
} catch (e) {
|
|
log(`Failed to connect to Geoclue2 service: ${e.message}`);
|
|
this._setLocation(this._mostRecentLocation);
|
|
return;
|
|
}
|
|
this._gclueStarted = true;
|
|
this._gclueService.get_client().distance_threshold = 100;
|
|
this._updateLocationMonitoring();
|
|
}
|
|
|
|
_onGClueLocationChanged() {
|
|
let geoLocation = this._gclueService.location;
|
|
// Provide empty name so GWeather sets location name
|
|
const location = GWeather.Location.new_detached('',
|
|
null,
|
|
geoLocation.latitude,
|
|
geoLocation.longitude);
|
|
this._setLocation(location);
|
|
}
|
|
|
|
_onAutomaticLocationChanged() {
|
|
let useAutoLocation = this._settings.get_boolean('automatic-location');
|
|
if (this._autoLocationRequested === useAutoLocation)
|
|
return;
|
|
|
|
this._autoLocationRequested = useAutoLocation;
|
|
|
|
this._updateAutoLocation();
|
|
}
|
|
|
|
_updateAutoLocation() {
|
|
this._updateLocationMonitoring();
|
|
|
|
if (this._useAutoLocation)
|
|
this._startGClueService();
|
|
else
|
|
this._setLocation(this._mostRecentLocation);
|
|
}
|
|
|
|
_onLocationsChanged() {
|
|
let locations = this._settings.get_value('locations').deepUnpack();
|
|
let serialized = locations.shift();
|
|
let mostRecentLocation = null;
|
|
|
|
if (serialized)
|
|
mostRecentLocation = this._world.deserialize(serialized);
|
|
|
|
if (this._locationsEqual(this._mostRecentLocation, mostRecentLocation))
|
|
return;
|
|
|
|
this._mostRecentLocation = mostRecentLocation;
|
|
|
|
if (!this._useAutoLocation || !this._gclueStarted)
|
|
this._setLocation(this._mostRecentLocation);
|
|
}
|
|
|
|
_onPermStoreChanged(proxy, sender, params) {
|
|
let [table, id, deleted_, data_, perms] = params;
|
|
|
|
if (table !== 'gnome' || id !== 'geolocation')
|
|
return;
|
|
|
|
let permission = perms['org.gnome.Weather'] || ['NONE'];
|
|
let [accuracy] = permission;
|
|
this._weatherAuthorized = accuracy !== 'NONE';
|
|
|
|
this._updateAutoLocation();
|
|
}
|
|
}
|