summaryrefslogtreecommitdiffstats
path: root/js/ui/screenShield.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/screenShield.js')
-rw-r--r--js/ui/screenShield.js686
1 files changed, 686 insertions, 0 deletions
diff --git a/js/ui/screenShield.js b/js/ui/screenShield.js
new file mode 100644
index 0000000..325fbff
--- /dev/null
+++ b/js/ui/screenShield.js
@@ -0,0 +1,686 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ScreenShield */
+
+const {
+ AccountsService, Clutter, Gio,
+ GLib, Graphene, Meta, Shell, St,
+} = imports.gi;
+const Signals = imports.misc.signals;
+
+const GnomeSession = imports.misc.gnomeSession;
+const OVirt = imports.gdm.oVirt;
+const LoginManager = imports.misc.loginManager;
+const Lightbox = imports.ui.lightbox;
+const Main = imports.ui.main;
+const Overview = imports.ui.overview;
+const MessageTray = imports.ui.messageTray;
+const ShellDBus = imports.ui.shellDBus;
+const SmartcardManager = imports.misc.smartcardManager;
+
+const { adjustAnimationTime } = imports.ui.environment;
+
+const SCREENSAVER_SCHEMA = 'org.gnome.desktop.screensaver';
+const LOCK_ENABLED_KEY = 'lock-enabled';
+const LOCK_DELAY_KEY = 'lock-delay';
+
+const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown';
+const DISABLE_LOCK_KEY = 'disable-lock-screen';
+
+const LOCKED_STATE_STR = 'screenShield.locked';
+
+// ScreenShield animation time
+// - STANDARD_FADE_TIME is used when the session goes idle
+// - MANUAL_FADE_TIME is used for lowering the shield when asked by the user,
+// or when cancelling the dialog
+// - CURTAIN_SLIDE_TIME is used when raising the shield before unlocking
+var STANDARD_FADE_TIME = 10000;
+var MANUAL_FADE_TIME = 300;
+var CURTAIN_SLIDE_TIME = 300;
+
+/**
+ * If you are setting org.gnome.desktop.session.idle-delay directly in dconf,
+ * rather than through System Settings, you also need to set
+ * org.gnome.settings-daemon.plugins.power.sleep-display-ac and
+ * org.gnome.settings-daemon.plugins.power.sleep-display-battery to the same value.
+ * This will ensure that the screen blanks at the right time when it fades out.
+ * https://bugzilla.gnome.org/show_bug.cgi?id=668703 explains the dependency.
+ */
+var ScreenShield = class extends Signals.EventEmitter {
+ constructor() {
+ super();
+
+ this.actor = Main.layoutManager.screenShieldGroup;
+
+ this._lockScreenState = MessageTray.State.HIDDEN;
+ this._lockScreenGroup = new St.Widget({
+ x_expand: true,
+ y_expand: true,
+ reactive: true,
+ can_focus: true,
+ name: 'lockScreenGroup',
+ visible: false,
+ });
+
+ this._lockDialogGroup = new St.Widget({
+ x_expand: true,
+ y_expand: true,
+ reactive: true,
+ can_focus: true,
+ pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
+ name: 'lockDialogGroup',
+ });
+
+ this.actor.add_actor(this._lockScreenGroup);
+ this.actor.add_actor(this._lockDialogGroup);
+
+ this._presence = new GnomeSession.Presence((proxy, error) => {
+ if (error) {
+ logError(error, 'Error while reading gnome-session presence');
+ return;
+ }
+
+ this._onStatusChanged(proxy.status);
+ });
+ this._presence.connectSignal('StatusChanged', (proxy, senderName, [status]) => {
+ this._onStatusChanged(status);
+ });
+
+ this._screenSaverDBus = new ShellDBus.ScreenSaverDBus(this);
+
+ this._smartcardManager = SmartcardManager.getSmartcardManager();
+ this._smartcardManager.connect('smartcard-inserted',
+ (manager, token) => {
+ if (this._isLocked && token.UsedToLogin)
+ this._activateDialog();
+ });
+
+ this._oVirtCredentialsManager = OVirt.getOVirtCredentialsManager();
+ this._oVirtCredentialsManager.connect('user-authenticated',
+ () => {
+ if (this._isLocked)
+ this._activateDialog();
+ });
+
+ this._loginManager = LoginManager.getLoginManager();
+ this._loginManager.connect('prepare-for-sleep',
+ this._prepareForSleep.bind(this));
+
+ this._loginSession = null;
+ this._getLoginSession();
+
+ this._settings = new Gio.Settings({ schema_id: SCREENSAVER_SCHEMA });
+ this._settings.connect(`changed::${LOCK_ENABLED_KEY}`, this._syncInhibitor.bind(this));
+
+ this._lockSettings = new Gio.Settings({ schema_id: LOCKDOWN_SCHEMA });
+ this._lockSettings.connect(`changed::${DISABLE_LOCK_KEY}`, this._syncInhibitor.bind(this));
+
+ this._isModal = false;
+ this._isGreeter = false;
+ this._isActive = false;
+ this._isLocked = false;
+ this._inUnlockAnimation = false;
+ this._inhibited = false;
+ this._activationTime = 0;
+ this._becameActiveId = 0;
+ this._lockTimeoutId = 0;
+
+ // The "long" lightbox is used for the longer (20 seconds) fade from session
+ // to idle status, the "short" is used for quickly fading to black when locking
+ // manually
+ this._longLightbox = new Lightbox.Lightbox(Main.uiGroup, {
+ inhibitEvents: true,
+ fadeFactor: 1,
+ });
+ this._longLightbox.connect('notify::active', this._onLongLightbox.bind(this));
+ this._shortLightbox = new Lightbox.Lightbox(Main.uiGroup, {
+ inhibitEvents: true,
+ fadeFactor: 1,
+ });
+ this._shortLightbox.connect('notify::active', this._onShortLightbox.bind(this));
+
+ this.idleMonitor = global.backend.get_core_idle_monitor();
+ this._cursorTracker = Meta.CursorTracker.get_for_display(global.display);
+
+ this._syncInhibitor();
+ }
+
+ async _getLoginSession() {
+ this._loginSession = await this._loginManager.getCurrentSessionProxy();
+ this._loginSession.connectSignal('Lock',
+ () => this.lock(false));
+ this._loginSession.connectSignal('Unlock',
+ () => this.deactivate(false));
+ this._loginSession.connect('g-properties-changed',
+ () => this._syncInhibitor());
+ this._syncInhibitor();
+ }
+
+ _setActive(active) {
+ let prevIsActive = this._isActive;
+ this._isActive = active;
+
+ if (prevIsActive != this._isActive)
+ this.emit('active-changed');
+
+ this._syncInhibitor();
+ }
+
+ _setLocked(locked) {
+ let prevIsLocked = this._isLocked;
+ this._isLocked = locked;
+
+ if (prevIsLocked !== this._isLocked)
+ this.emit('locked-changed');
+
+ if (this._loginSession)
+ this._loginSession.SetLockedHintAsync(locked).catch(logError);
+ }
+
+ _activateDialog() {
+ if (this._isLocked) {
+ this._ensureUnlockDialog(true /* allowCancel */);
+ this._dialog.activate();
+ } else {
+ this.deactivate(true /* animate */);
+ }
+ }
+
+ _maybeCancelDialog() {
+ if (!this._dialog)
+ return;
+
+ this._dialog.cancel();
+ if (this._isGreeter) {
+ // LoginDialog.cancel() will grab the key focus
+ // on its own, so ensure it stays on lock screen
+ // instead
+ this._dialog.grab_key_focus();
+ }
+ }
+
+ _becomeModal() {
+ if (this._isModal)
+ return true;
+
+ let grab = Main.pushModal(Main.uiGroup, { actionMode: Shell.ActionMode.LOCK_SCREEN });
+
+ // We expect at least a keyboard grab here
+ this._isModal = (grab.get_seat_state() & Clutter.GrabState.KEYBOARD) !== 0;
+ if (this._isModal)
+ this._grab = grab;
+ else
+ Main.popModal(grab);
+
+ return this._isModal;
+ }
+
+ async _syncInhibitor() {
+ const lockEnabled = this._settings.get_boolean(LOCK_ENABLED_KEY);
+ const lockLocked = this._lockSettings.get_boolean(DISABLE_LOCK_KEY);
+ const inhibit = !!this._loginSession && this._loginSession.Active &&
+ !this._isActive && lockEnabled && !lockLocked &&
+ !!Main.sessionMode.unlockDialog;
+
+ if (inhibit === this._inhibited)
+ return;
+
+ this._inhibited = inhibit;
+
+ this._inhibitCancellable?.cancel();
+ this._inhibitCancellable = new Gio.Cancellable();
+
+ if (inhibit) {
+ try {
+ this._inhibitor = await this._loginManager.inhibit(
+ _('GNOME needs to lock the screen'),
+ this._inhibitCancellable);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ log('Failed to inhibit suspend: %s'.format(e.message));
+ }
+ } else {
+ this._inhibitor?.close(null);
+ this._inhibitor = null;
+ }
+ }
+
+ _prepareForSleep(loginManager, aboutToSuspend) {
+ if (aboutToSuspend) {
+ if (this._settings.get_boolean(LOCK_ENABLED_KEY))
+ this.lock(true);
+ } else {
+ this._wakeUpScreen();
+ }
+ }
+
+ _onStatusChanged(status) {
+ if (status != GnomeSession.PresenceStatus.IDLE)
+ return;
+
+ this._maybeCancelDialog();
+
+ if (this._longLightbox.visible) {
+ // We're in the process of showing.
+ return;
+ }
+
+ if (!this._becomeModal()) {
+ // We could not become modal, so we can't activate the
+ // screenshield. The user is probably very upset at this
+ // point, but any application using global grabs is broken
+ // Just tell them to stop using this app
+ //
+ // XXX: another option is to kick the user into the gdm login
+ // screen, where we're not affected by grabs
+ Main.notifyError(_("Unable to lock"),
+ _("Lock was blocked by an application"));
+ return;
+ }
+
+ if (this._activationTime == 0)
+ this._activationTime = GLib.get_monotonic_time();
+
+ let shouldLock = this._settings.get_boolean(LOCK_ENABLED_KEY) && !this._isLocked;
+
+ if (shouldLock) {
+ let lockTimeout = Math.max(
+ adjustAnimationTime(STANDARD_FADE_TIME),
+ this._settings.get_uint(LOCK_DELAY_KEY) * 1000);
+ this._lockTimeoutId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ lockTimeout,
+ () => {
+ this._lockTimeoutId = 0;
+ this.lock(false);
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(this._lockTimeoutId, '[gnome-shell] this.lock');
+ }
+
+ this._activateFade(this._longLightbox, STANDARD_FADE_TIME);
+ }
+
+ _activateFade(lightbox, time) {
+ Main.uiGroup.set_child_above_sibling(lightbox, null);
+ lightbox.lightOn(time);
+
+ if (this._becameActiveId == 0)
+ this._becameActiveId = this.idleMonitor.add_user_active_watch(this._onUserBecameActive.bind(this));
+ }
+
+ _onUserBecameActive() {
+ // This function gets called here when the user becomes active
+ // after we activated a lightbox
+ // There are two possibilities here:
+ // - we're called when already locked; we just go back to the lock screen curtain
+ // - we're called because the session is IDLE but before the lightbox
+ // is fully shown; at this point isActive is false, so we just hide
+ // the lightbox, reset the activationTime and go back to the unlocked
+ // desktop
+ // using deactivate() is a little of overkill, but it ensures we
+ // don't forget of some bit like modal, DBus properties or idle watches
+ //
+ // Note: if the (long) lightbox is shown then we're necessarily
+ // active, because we call activate() without animation.
+
+ this.idleMonitor.remove_watch(this._becameActiveId);
+ this._becameActiveId = 0;
+
+ if (this._isLocked) {
+ this._longLightbox.lightOff();
+ this._shortLightbox.lightOff();
+ } else {
+ this.deactivate(false);
+ }
+ }
+
+ _onLongLightbox(lightBox) {
+ if (lightBox.active)
+ this.activate(false);
+ }
+
+ _onShortLightbox(lightBox) {
+ if (lightBox.active)
+ this._completeLockScreenShown();
+ }
+
+ showDialog() {
+ if (!this._becomeModal()) {
+ // In the login screen, this is a hard error. Fail-whale
+ const error = new GLib.Error(
+ Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED,
+ 'Could not acquire modal grab for the login screen. Aborting login process.');
+ global.context.terminate_with_error(error);
+ }
+
+ this.actor.show();
+ this._isGreeter = Main.sessionMode.isGreeter;
+ this._isLocked = true;
+ this._ensureUnlockDialog(true);
+ }
+
+ _hideLockScreenComplete() {
+ this._lockScreenState = MessageTray.State.HIDDEN;
+ this._lockScreenGroup.hide();
+
+ if (this._dialog) {
+ this._dialog.grab_key_focus();
+ this._dialog.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
+ }
+ }
+
+ _showPointer() {
+ this._cursorTracker.set_pointer_visible(true);
+
+ if (this._motionId) {
+ global.stage.disconnect(this._motionId);
+ this._motionId = 0;
+ }
+ }
+
+ _hidePointerUntilMotion() {
+ this._motionId = global.stage.connect('captured-event', (stage, event) => {
+ if (event.type() === Clutter.EventType.MOTION)
+ this._showPointer();
+
+ return Clutter.EVENT_PROPAGATE;
+ });
+ this._cursorTracker.set_pointer_visible(false);
+ }
+
+ _hideLockScreen(animate) {
+ if (this._lockScreenState == MessageTray.State.HIDDEN)
+ return;
+
+ this._lockScreenState = MessageTray.State.HIDING;
+
+ this._lockDialogGroup.remove_all_transitions();
+
+ if (animate) {
+ // Animate the lock screen out of screen
+ // if velocity is not specified (i.e. we come here from pressing ESC),
+ // use the same speed regardless of original position
+ // if velocity is specified, it's in pixels per milliseconds
+ let h = global.stage.height;
+ let delta = h + this._lockDialogGroup.translation_y;
+ let velocity = global.stage.height / CURTAIN_SLIDE_TIME;
+ let duration = delta / velocity;
+
+ this._lockDialogGroup.ease({
+ translation_y: -h,
+ duration,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => this._hideLockScreenComplete(),
+ });
+ } else {
+ this._hideLockScreenComplete();
+ }
+
+ this._showPointer();
+ }
+
+ _ensureUnlockDialog(allowCancel) {
+ if (!this._dialog) {
+ let constructor = Main.sessionMode.unlockDialog;
+ if (!constructor) {
+ // This session mode has no locking capabilities
+ this.deactivate(true);
+ return false;
+ }
+
+ this._dialog = new constructor(this._lockDialogGroup);
+
+ let time = global.get_current_time();
+ if (!this._dialog.open(time)) {
+ // This is kind of an impossible error: we're already modal
+ // by the time we reach this...
+ log('Could not open login dialog: failed to acquire grab');
+ this.deactivate(true);
+ return false;
+ }
+
+ this._dialog.connect('failed', this._onUnlockFailed.bind(this));
+ this._wakeUpScreenId = this._dialog.connect(
+ 'wake-up-screen', this._wakeUpScreen.bind(this));
+ }
+
+ this._dialog.allowCancel = allowCancel;
+ this._dialog.grab_key_focus();
+ return true;
+ }
+
+ _onUnlockFailed() {
+ this._resetLockScreen({
+ animateLockScreen: true,
+ fadeToBlack: false,
+ });
+ }
+
+ _resetLockScreen(params) {
+ // Don't reset the lock screen unless it is completely hidden
+ // This prevents the shield going down if the lock-delay timeout
+ // fires while the user is dragging (which has the potential
+ // to confuse our state)
+ if (this._lockScreenState != MessageTray.State.HIDDEN)
+ return;
+
+ this._lockScreenGroup.show();
+ this._lockScreenState = MessageTray.State.SHOWING;
+
+ let fadeToBlack = params.fadeToBlack;
+
+ if (params.animateLockScreen) {
+ this._lockDialogGroup.translation_y = -global.screen_height;
+ this._lockDialogGroup.remove_all_transitions();
+ this._lockDialogGroup.ease({
+ translation_y: 0,
+ duration: Overview.ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this._lockScreenShown({ fadeToBlack, animateFade: true });
+ },
+ });
+ } else {
+ this._lockDialogGroup.translation_y = 0;
+ this._lockScreenShown({ fadeToBlack, animateFade: false });
+ }
+
+ this._dialog.grab_key_focus();
+ }
+
+ _lockScreenShown(params) {
+ this._hidePointerUntilMotion();
+
+ this._lockScreenState = MessageTray.State.SHOWN;
+
+ if (params.fadeToBlack && params.animateFade) {
+ // Take a beat
+
+ let id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, MANUAL_FADE_TIME, () => {
+ this._activateFade(this._shortLightbox, MANUAL_FADE_TIME);
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(id, '[gnome-shell] this._activateFade');
+ } else {
+ if (params.fadeToBlack)
+ this._activateFade(this._shortLightbox, 0);
+
+ this._completeLockScreenShown();
+ }
+ }
+
+ _completeLockScreenShown() {
+ this._setActive(true);
+ this.emit('lock-screen-shown');
+ }
+
+ _wakeUpScreen() {
+ if (!this.active)
+ return; // already woken up, or not yet asleep
+ this._onUserBecameActive();
+ this.emit('wake-up-screen');
+ }
+
+ get locked() {
+ return this._isLocked;
+ }
+
+ get active() {
+ return this._isActive;
+ }
+
+ get activationTime() {
+ return this._activationTime;
+ }
+
+ deactivate(animate) {
+ if (this._dialog)
+ this._dialog.finish(() => this._continueDeactivate(animate));
+ else
+ this._continueDeactivate(animate);
+ }
+
+ _continueDeactivate(animate) {
+ this._hideLockScreen(animate);
+
+ if (Main.sessionMode.currentMode == 'unlock-dialog')
+ Main.sessionMode.popMode('unlock-dialog');
+
+ this.emit('wake-up-screen');
+
+ if (this._isGreeter) {
+ // We don't want to "deactivate" any more than
+ // this. In particular, we don't want to drop
+ // the modal, hide ourselves or destroy the dialog
+ // But we do want to set isActive to false, so that
+ // gnome-session will reset the idle counter, and
+ // gnome-settings-daemon will stop blanking the screen
+
+ this._activationTime = 0;
+ this._setActive(false);
+ return;
+ }
+
+ if (this._dialog && !this._isGreeter)
+ this._dialog.popModal();
+
+ if (this._isModal) {
+ Main.popModal(this._grab);
+ this._grab = null;
+ this._isModal = false;
+ }
+
+ this._longLightbox.lightOff();
+ this._shortLightbox.lightOff();
+
+ this._lockDialogGroup.ease({
+ translation_y: -global.screen_height,
+ duration: Overview.ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => this._completeDeactivate(),
+ });
+ }
+
+ _completeDeactivate() {
+ if (this._dialog) {
+ this._dialog.destroy();
+ this._dialog = null;
+ }
+
+ this.actor.hide();
+
+ if (this._becameActiveId != 0) {
+ this.idleMonitor.remove_watch(this._becameActiveId);
+ this._becameActiveId = 0;
+ }
+
+ if (this._lockTimeoutId != 0) {
+ GLib.source_remove(this._lockTimeoutId);
+ this._lockTimeoutId = 0;
+ }
+
+ this._activationTime = 0;
+ this._setActive(false);
+ this._setLocked(false);
+ global.set_runtime_state(LOCKED_STATE_STR, null);
+ }
+
+ activate(animate) {
+ if (this._activationTime == 0)
+ this._activationTime = GLib.get_monotonic_time();
+
+ if (!this._ensureUnlockDialog(true))
+ return;
+
+ this.actor.show();
+
+ if (Main.sessionMode.currentMode !== 'unlock-dialog') {
+ this._isGreeter = Main.sessionMode.isGreeter;
+ if (!this._isGreeter)
+ Main.sessionMode.pushMode('unlock-dialog');
+ }
+
+ this._resetLockScreen({
+ animateLockScreen: animate,
+ fadeToBlack: true,
+ });
+ // On wayland, a crash brings down the entire session, so we don't
+ // need to defend against being restarted unlocked
+ if (!Meta.is_wayland_compositor())
+ global.set_runtime_state(LOCKED_STATE_STR, GLib.Variant.new('b', true));
+
+ // We used to set isActive and emit active-changed here,
+ // but now we do that from lockScreenShown, which means
+ // there is a 0.3 seconds window during which the lock
+ // screen is effectively visible and the screen is locked, but
+ // the DBus interface reports the screensaver is off.
+ // This is because when we emit ActiveChanged(true),
+ // gnome-settings-daemon blanks the screen, and we don't want
+ // blank during the animation.
+ // This is not a problem for the idle fade case, because we
+ // activate without animation in that case.
+ }
+
+ lock(animate) {
+ if (this._lockSettings.get_boolean(DISABLE_LOCK_KEY)) {
+ log('Screen lock is locked down, not locking'); // lock, lock - who's there?
+ return;
+ }
+
+ // Warn the user if we can't become modal
+ if (!this._becomeModal()) {
+ Main.notifyError(_("Unable to lock"),
+ _("Lock was blocked by an application"));
+ return;
+ }
+
+ // Clear the clipboard - otherwise, its contents may be leaked
+ // to unauthorized parties by pasting into the unlock dialog's
+ // password entry and unmasking the entry
+ St.Clipboard.get_default().set_text(St.ClipboardType.CLIPBOARD, '');
+ St.Clipboard.get_default().set_text(St.ClipboardType.PRIMARY, '');
+
+ let userManager = AccountsService.UserManager.get_default();
+ let user = userManager.get_user(GLib.get_user_name());
+
+ this.activate(animate);
+
+ const lock = this._isGreeter
+ ? true
+ : user.password_mode !== AccountsService.UserPasswordMode.NONE;
+ this._setLocked(lock);
+ }
+
+ // If the previous shell crashed, and gnome-session restarted us, then re-lock
+ lockIfWasLocked() {
+ if (!this._settings.get_boolean(LOCK_ENABLED_KEY))
+ return;
+ let wasLocked = global.get_runtime_state('b', LOCKED_STATE_STR);
+ if (wasLocked === null)
+ return;
+ Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
+ this.lock(false);
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+};