Introduce Automatic Updates component
https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/466
This commit is contained in:
@ -9,6 +9,7 @@
|
||||
<file>gdm/realmd.js</file>
|
||||
<file>gdm/util.js</file>
|
||||
|
||||
<file>misc/updateManager.js</file>
|
||||
<file>misc/config.js</file>
|
||||
<file>misc/extensionUtils.js</file>
|
||||
<file>misc/fileUtils.js</file>
|
||||
@ -116,6 +117,7 @@
|
||||
<file>ui/components/networkAgent.js</file>
|
||||
<file>ui/components/polkitAgent.js</file>
|
||||
<file>ui/components/telepathyClient.js</file>
|
||||
<file>ui/components/updates.js</file>
|
||||
<file>ui/components/keyring.js</file>
|
||||
|
||||
<file>ui/status/accessibility.js</file>
|
||||
|
338
js/misc/updateManager.js
Normal file
338
js/misc/updateManager.js
Normal file
@ -0,0 +1,338 @@
|
||||
// -*- 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 = '\
|
||||
<node> \
|
||||
<interface name="com.endlessm.DownloadManager1.Scheduler"> \
|
||||
<property name="ActiveEntryCount" type="u" access="read" /> \
|
||||
<property name="EntryCount" type="u" access="read" /> \
|
||||
</interface> \
|
||||
</node>';
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
135
js/ui/components/updates.js
Normal file
135
js/ui/components/updates.js
Normal file
@ -0,0 +1,135 @@
|
||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||
//
|
||||
// Copyright (C) 2018 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 { Gio, GLib, Shell } = imports.gi;
|
||||
|
||||
const UpdateManager = imports.misc.updateManager;
|
||||
|
||||
const Main = imports.ui.main;
|
||||
const MessageTray = imports.ui.messageTray;
|
||||
|
||||
var UpdateComponent = class {
|
||||
constructor() {
|
||||
this._notification = null;
|
||||
this._state = UpdateManager.State.UNKNOWN;
|
||||
|
||||
this._manager = UpdateManager.getUpdateManager();
|
||||
this._manager.connect('notify::state', this._updateState.bind(this));
|
||||
|
||||
this._updateState();
|
||||
}
|
||||
|
||||
enable() {
|
||||
}
|
||||
|
||||
disable() {
|
||||
}
|
||||
|
||||
_updateState() {
|
||||
let newState = this._manager.state;
|
||||
|
||||
if (this._state == newState)
|
||||
return;
|
||||
|
||||
this._updateNotification(newState);
|
||||
this._state = newState;
|
||||
}
|
||||
|
||||
_updateNotification(newState) {
|
||||
// Don't notify when starting up
|
||||
if (this._manager.state == UpdateManager.State.UNKNOWN)
|
||||
return;
|
||||
|
||||
let alreadySentNotification = this._manager.lastNotificationTime != -1;
|
||||
|
||||
let wasDisconnected = this._state == UpdateManager.State.DISCONNECTED;
|
||||
let wasActive = this._state >= UpdateManager.State.IDLE;
|
||||
let isActive = newState >= UpdateManager.State.IDLE;
|
||||
|
||||
// The criteria to notify about the Automatic Updates setting is:
|
||||
// 1. If the user was disconnected and connects to a new network; or
|
||||
// 2. If the user was connected and connects to a network with different status;
|
||||
if ((wasDisconnected && alreadySentNotification) || (!wasDisconnected && isActive == wasActive))
|
||||
return;
|
||||
|
||||
if (this._notification)
|
||||
this._notification.destroy();
|
||||
|
||||
if (newState == UpdateManager.State.DISCONNECTED)
|
||||
return;
|
||||
|
||||
let source = new MessageTray.SystemNotificationSource();
|
||||
Main.messageTray.add(source);
|
||||
|
||||
// Figure out the title, subtitle and icon
|
||||
let title, subtitle, iconFile;
|
||||
|
||||
if (isActive) {
|
||||
title = _("Automatic updates on");
|
||||
subtitle = _("Your connection has unlimited data so automatic updates have been turned on.");
|
||||
iconFile = UpdateManager.stateToIconName(UpdateManager.State.IDLE);
|
||||
} else {
|
||||
title = _("Automatic updates are turned off to save your data");
|
||||
subtitle = _("You will need to choose which updates to apply when on this connection.");
|
||||
iconFile = UpdateManager.stateToIconName(UpdateManager.State.DISABLED);
|
||||
}
|
||||
|
||||
let gicon = new Gio.FileIcon({ file: Gio.File.new_for_uri(iconFile) });
|
||||
|
||||
// Create the notification.
|
||||
// The first time we notify the user for a given connection,
|
||||
// we set the urgency to critical so that we make sure the
|
||||
// user understands how we may be changing their settings.
|
||||
// On subsequent notifications for the given connection,
|
||||
// for instance if the user regularly switches between
|
||||
// metered and unmetered connections, we set the urgency
|
||||
// to normal so as not to be too obtrusive.
|
||||
this._notification = new MessageTray.Notification(source, title, subtitle, { gicon: gicon });
|
||||
this._notification.setUrgency(alreadySentNotification ?
|
||||
MessageTray.Urgency.NORMAL : MessageTray.Urgency.CRITICAL);
|
||||
this._notification.setTransient(false);
|
||||
|
||||
this._notification.addAction(_("Close"), () => {
|
||||
this._notification.destroy();
|
||||
});
|
||||
|
||||
this._notification.addAction(_("Change Settings…"), () => {
|
||||
// FIXME: this requires the Automatic Updates panel in GNOME
|
||||
// Settings. Going with the Network panel for now…
|
||||
let app = Shell.AppSystem.get_default().lookup_app('gnome-network-panel.desktop');
|
||||
Main.overview.hide();
|
||||
app.activate();
|
||||
});
|
||||
|
||||
source.notify(this._notification);
|
||||
|
||||
this._notification.connect('destroy', () => {
|
||||
this._notification = null;
|
||||
});
|
||||
|
||||
// Now that we first detected this connection, mark it as such
|
||||
this._manager.lastNotificationTime = GLib.get_real_time();
|
||||
}
|
||||
};
|
||||
|
||||
var Component = UpdateComponent;
|
@ -92,9 +92,11 @@ const _modes = {
|
||||
unlockDialog: imports.ui.unlockDialog.UnlockDialog,
|
||||
components: Config.HAVE_NETWORKMANAGER ?
|
||||
['networkAgent', 'polkitAgent', 'telepathyClient',
|
||||
'keyring', 'autorunManager', 'automountManager'] :
|
||||
'keyring', 'autorunManager', 'automountManager',
|
||||
'updates'] :
|
||||
['polkitAgent', 'telepathyClient',
|
||||
'keyring', 'autorunManager', 'automountManager'],
|
||||
'keyring', 'autorunManager', 'automountManager',
|
||||
'updates'],
|
||||
|
||||
panel: {
|
||||
left: ['activities', 'appMenu'],
|
||||
|
Reference in New Issue
Block a user