// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- // // Copyright (C) 2019 Endless Mobile, Inc. // // This is a GNOME Shell component to wrap the interactions over // D-Bus with the Mogwai system daemon. // // Licensed under the GNU General Public License Version 2 // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. const { Clutter, Gio, GLib, GObject, Gtk, NM, Shell, St } = imports.gi; const NM_SETTING_AUTOMATIC_UPDATES_NOTIFICATION_TIME = "connection.automatic-updates-notification-time"; const NM_SETTING_ALLOW_DOWNLOADS = 'connection.allow-downloads'; const NM_SETTING_TARIFF_ENABLED = "connection.tariff-enabled"; const SchedulerInterface = '\ \ \ \ \ \ '; const SchedulerProxy = Gio.DBusProxy.makeProxyWrapper(SchedulerInterface); let _updateManager = null; function getUpdateManager() { if (_updateManager == null) _updateManager = new UpdateManager(); return _updateManager; } var State = { UNKNOWN: 0, DISCONNECTED: 1, DISABLED: 2, IDLE: 3, SCHEDULED: 4, DOWNLOADING: 5 }; function stateToIconName(state) { switch (state) { case State.UNKNOWN: case State.DISCONNECTED: return null; case State.DISABLED: return 'resource:///org/gnome/shell/theme/automatic-updates-off-symbolic.svg'; case State.IDLE: case State.DOWNLOADING: return 'resource:///org/gnome/shell/theme/automatic-updates-on-symbolic.svg'; case State.SCHEDULED: return 'resource:///org/gnome/shell/theme/automatic-updates-scheduled-symbolic.svg'; } return null; } var UpdateManager = GObject.registerClass ({ Properties: { 'last-notification-time': GObject.ParamSpec.int('last-notification-time', 'last-notification-time', 'last-notification-time', GObject.ParamFlags.READWRITE, null), 'icon': GObject.ParamSpec.object('icon', 'icon', 'icon', GObject.ParamFlags.READABLE, Gio.Icon.$gtype), 'state': GObject.ParamSpec.uint('state', 'state', 'state', GObject.ParamFlags.READABLE, null), }, }, class UpdateManager extends GObject.Object { _init() { super._init(); this._activeConnection = null; this._settingChangedSignalId = 0; this._updateTimeoutId = 0; this._state = State.UNKNOWN; NM.Client.new_async(null, this._clientGot.bind(this)); } _clientGot(obj, result) { this._client = NM.Client.new_finish(result); this._client.connect('notify::primary-connection', this._sync.bind(this)); this._client.connect('notify::state', this._sync.bind(this)); // Start retrieving the Mogwai proxy this._proxy = new SchedulerProxy(Gio.DBus.system, 'com.endlessm.MogwaiSchedule1', '/com/endlessm/DownloadManager1', (proxy, error) => { if (error) { log(error.message); return; } this._proxy.connect('g-properties-changed', this._sync.bind(this)); this._updateStatus(); }); this._sync(); } _sync() { if (!this._client || !this._proxy) return; if (this._updateTimeoutId > 0) { GLib.source_remove(this._updateTimeoutId); this._updateTimeoutId = 0; } // Intermediate states (connecting or disconnecting) must not trigger // any kind of state change. if (this._client.state == NM.State.CONNECTING || this._client.state == NM.State.DISCONNECTING) return; // Use a timeout to avoid instantly throwing the notification at // the user's face, and to avoid a series of unecessary updates // that happen when NetworkManager is still figuring out details. this._updateTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => { this._updateStatus(); this._updateTimeoutId = 0; return GLib.SOURCE_REMOVE; }); GLib.Source.set_name_by_id(this._updateTimeoutId, '[update] updateStatus'); } _updateStatus() { // Update the current active connection. This will connect to the // NM.SettingUser signal to sync every time someone updates the // NM_SETTING_ALLOW_DOWNLOADS setting. this._updateActiveConnection(); let state = this._getState(); if (state != this._state) { this._state = state; this.notify('state'); this._updateIcon(); } } _updateActiveConnection() { let currentActiveConnection = this._getActiveConnection(); if (this._activeConnection == currentActiveConnection) return; // Disconnect from the previous active connection if (this._settingChangedSignalId > 0) { this._activeConnection.disconnect(this._settingChangedSignalId); this._settingChangedSignalId = 0; } this._activeConnection = currentActiveConnection; // Connect from the current active connection if (currentActiveConnection) this._settingChangedSignalId = currentActiveConnection.connect('changed', this._updateStatus.bind(this)); } _ensureUserSetting(connection) { let userSetting = connection.get_setting(NM.SettingUser.$gtype); if (!userSetting) { userSetting = new NM.SettingUser(); connection.add_setting(userSetting); } return userSetting; } _getState() { if (!this._activeConnection) return State.DISCONNECTED; let userSetting = this._ensureUserSetting(this._activeConnection); // We only return true when: // * Automatic Updates are on // * A schedule was set // * Something is being downloaded let allowDownloadsValue = userSetting.get_data(NM_SETTING_ALLOW_DOWNLOADS); if (allowDownloadsValue) { let allowDownloads = (allowDownloadsValue === '1'); if (!allowDownloads) return State.DISABLED; } else { // Guess the default value from the metered state. Only return // if it's disabled - if it's not, we want to follow the regular // code paths and fetch the correct state let connectionSetting = this._activeConnection.get_setting_connection(); if (!connectionSetting) return State.DISABLED; let metered = connectionSetting.get_metered(); if (metered == NM.Metered.YES || metered == NM.Metered.GUESS_YES) return State.DISABLED; } // Without the proxy, we can't really know the state if (!this._proxy) return State.UNKNOWN; let scheduleSet = userSetting.get_data(NM_SETTING_TARIFF_ENABLED) === '1'; if (!scheduleSet) return State.IDLE; let downloading = this._proxy.ActiveEntryCount > 0; if (downloading) return State.DOWNLOADING; // At this point we're not downloading anything, but something // might be queued let downloadsQueued = this._proxy.EntryCount > 0; if (downloadsQueued) return State.SCHEDULED; else return State.IDLE; } _getActiveConnection() { let activeConnection = this._client.get_primary_connection(); return activeConnection ? activeConnection.get_connection() : null; } _updateIcon() { let state = this._state; let iconName = stateToIconName(state); if (iconName) { let iconFile = Gio.File.new_for_uri(iconName); this._icon = new Gio.FileIcon({ file: iconFile }); } else { this._icon = null; } this.notify('icon'); } get state() { return this._state; } get lastNotificationTime() { let connection = this._getActiveConnection(); if (!connection) return -1; let userSetting = connection.get_setting(NM.SettingUser.$gtype); if (!userSetting) return -1; let time = userSetting.get_data(NM_SETTING_AUTOMATIC_UPDATES_NOTIFICATION_TIME); return time ? parseInt(time) : -1; } set lastNotificationTime(time) { if (!this._activeConnection) return; let userSetting = this._ensureUserSetting(this._activeConnection); userSetting.set_data(NM_SETTING_AUTOMATIC_UPDATES_NOTIFICATION_TIME, '%s'.format(time)); this._activeConnection.commit_changes(true, null); } get active() { return this._active; } set active(_active) { if (this._active == _active) return; this._active = _active; this.notify('active'); } get icon() { return this._icon; } toggleAutomaticUpdates() { if (!this._activeConnection) return; let userSetting = this._ensureUserSetting(this._activeConnection); let state = this._getState(); let value; if (state == State.IDLE || state == State.SCHEDULED || state == State.DOWNLOADING) { value = '0'; } else { value = '1'; } userSetting.set_data(NM_SETTING_ALLOW_DOWNLOADS, value); this._activeConnection.commit_changes_async(true, null, (con, res, data) => { this._activeConnection.commit_changes_finish(res); this._updateStatus(); }); } });