Blob Blame History Raw
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

const { Geoclue, Gio, GLib, GWeather } = imports.gi;
const Signals = imports.signals;

const PermissionStore = imports.misc.permissionStore;
const Util = imports.misc.util;

// Minimum time between updates to show loading indication
var UPDATE_THRESHOLD = 10 * GLib.TIME_SPAN_MINUTE;

var WeatherClient = class {
    constructor() {
        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._weatherAuthorized = false;
        this._permStore = new PermissionStore.PermissionStore((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;
            }

            this._permStore.LookupRemote('gnome', 'geolocation', (res, error) => {
                if (error)
                    log('Error looking up permission: ' + error.message);

                let [perms, data] = error ? [{}, null] : res;
                let  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();

        this._providers = GWeather.Provider.METAR |
                          GWeather.Provider.YR_NO |
                          GWeather.Provider.OWM;

        this._weatherInfo = new GWeather.Info({ enabled_providers: 0 });
        this._weatherInfo.connect_after('updated', () => {
            this._lastUpdate = GLib.DateTime.new_now_local();
            this.emit('changed');
        });

        this._weatherAppMon = new Util.AppSettingsMonitor('org.gnome.Weather.desktop',
                                                          'org.gnome.Weather');
        this._weatherAppMon.connect('available-changed', () => { this.emit('changed'); });
        this._weatherAppMon.watchSetting('automatic-location',
                                         this._onAutomaticLocationChanged.bind(this));
        this._weatherAppMon.watchSetting('locations',
                                         this._onLocationsChanged.bind(this));
    }

    get available() {
        return this._weatherAppMon.available;
    }

    get loading() {
        return this._loading;
    }

    get hasLocation() {
        return this._locationValid;
    }

    get info() {
        return this._weatherInfo;
    }

    activateApp() {
        this._weatherAppMon.activateApp();
    }

    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._weatherAuthorized;
    }

    _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);

        this._weatherInfo.set_enabled_providers(location ? this._providers : 0);

        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;
        }
    }

    _startGClueService() {
        if (this._gclueStarting)
            return;

        this._gclueStarting = true;

        Geoclue.Simple.new('org.gnome.Shell', Geoclue.AccuracyLevel.CITY, null,
            (o, res) => {
                try {
                    this._gclueService = Geoclue.Simple.new_finish(res);
                } 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;
        let location = GWeather.Location.new_detached(geoLocation.description,
                                                      null,
                                                      geoLocation.latitude,
                                                      geoLocation.longitude);
        this._setLocation(location);
    }

    _onAutomaticLocationChanged(settings, key) {
        let useAutoLocation = settings.get_boolean(key);
        if (this._autoLocationRequested == useAutoLocation)
            return;

        this._autoLocationRequested = useAutoLocation;

        this._updateAutoLocation();
    }

    _updateAutoLocation() {
        this._updateLocationMonitoring();

        if (this._useAutoLocation)
            this._startGClueService();
        else
            this._setLocation(this._mostRecentLocation);
    }

    _onLocationsChanged(settings, key) {
        let serialized = settings.get_value(key).deep_unpack().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();
    }
};
Signals.addSignalMethods(WeatherClient.prototype);