2 * Copyright (C) 2013-2016 Canonical Ltd.
3 * Copyright (C) 2021 UBports Foundation
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; version 3.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19import AccountsService 0.1
23import Lomiri.Components 1.3
24import Lomiri.Launcher 0.1
25import Lomiri.Session 0.1
33 created: loader.status == Loader.Ready
35 property real dragHandleLeftMargin: 0
37 property url background
38 property bool hasCustomBackground
39 property real backgroundSourceSize
41 // How far to offset the top greeter layer during a launcher left-drag
42 property real launcherOffset
44 // How far down to position the greeter's interface to avoid the Panel
45 property real panelHeight
47 readonly property bool active: required || hasLockedApp
48 readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
50 property bool allowFingerprint: true
52 // True when the greeter is waiting for PAM or other setup process
53 readonly property alias waiting: d.waiting
55 property string lockedApp: ""
56 readonly property bool hasLockedApp: lockedApp !== ""
58 property bool forcedUnlock
59 readonly property bool locked: LightDMService.greeter.active && !LightDMService.greeter.authenticated && !forcedUnlock
61 property bool tabletMode
62 property string usageMode
63 property url viewSource // only used for testing
65 property int failedLoginsDelayAttempts: 7 // number of failed logins
66 property real failedLoginsDelayMinutes: 5 // minutes of forced waiting
67 property int failedFingerprintLoginsDisableAttempts: 5 // number of failed fingerprint logins
68 property int failedFingerprintReaderRetryDelay: 250 // time to wait before retrying a failed fingerprint read [ms]
70 readonly property bool animating: loader.item ? loader.item.animating : false
72 property rect inputMethodRect
74 property bool hasKeyboard: false
75 property int orientation
78 signal sessionStarted()
79 signal emergencyCall()
81 function forceShow() {
83 d.isLockscreen = true;
88 loader.item.forceShow();
90 // Normally loader.onLoaded will select a user, but if we're
91 // already shown, do it manually.
92 d.selectUser(d.currentIndex);
95 // Even though we may already be shown, we want to call show() for its
96 // possible side effects, like hiding indicators and such.
98 // We re-check forcedUnlock here, because selectUser above might
99 // process events during authentication, and a request to unlock could
100 // have come in in the meantime.
106 function notifyAppFocusRequested(appId) {
112 if (appId === lockedApp) {
113 hide(); // show locked app
116 d.startUnlock(false /* toTheRight */);
119 d.startUnlock(false /* toTheRight */);
123 // Notify that the user has explicitly requested an app
124 function notifyUserRequestedApp() {
129 // A hint that we're about to focus an app. This way we can look
130 // a little more responsive, rather than waiting for the above
131 // notifyAppFocusRequested call. We also need this in case we have a locked
132 // app, in order to show lockscreen instead of new app.
133 d.startUnlock(false /* toTheRight */);
136 // This is a just a glorified notifyUserRequestedApp(), but it does one
137 // other thing: it hides any cover pages to the RIGHT, because the user
138 // just came from a launcher drag starting on the left.
139 // It also returns a boolean value, indicating whether there was a visual
140 // change or not (the shell only wants to hide the launcher if there was
142 function notifyShowingDashFromDrag() {
147 return d.startUnlock(true /* toTheRight */);
150 function sessionToStart() {
151 for (var i = 0; i < LightDMService.sessions.count; i++) {
152 var session = LightDMService.sessions.data(i,
153 LightDMService.sessionRoles.KeyRole);
154 if (loader.item.sessionToStart === session) {
159 return LightDMService.greeter.defaultSession;
165 readonly property bool multiUser: LightDMService.users.count > 1
166 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
167 property int currentIndex: Math.max(selectUserIndex, 0)
168 readonly property bool waiting: LightDMService.prompts.count == 0 && !root.forcedUnlock
169 property bool isLockscreen // true when we are locking an active session, rather than first user login
170 readonly property bool secureFingerprint: isLockscreen &&
171 AccountsService.failedFingerprintLogins <
172 root.failedFingerprintLoginsDisableAttempts
173 readonly property bool alphanumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
175 // We want 'launcherOffset' to animate down to zero. But not to animate
176 // while being dragged. So ideally we change this only when the user
177 // lets go and launcherOffset drops to zero. But we need to wait for
178 // the behavior to be enabled first. So we cache the last known good
179 // launcherOffset value to cover us during that brief gap between
180 // release and the behavior turning on.
181 property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
182 property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
183 Behavior on launcherOffsetProxy {
184 id: launcherOffsetProxyBehavior
185 enabled: launcherOffset === 0
186 LomiriNumberAnimation {}
189 function getUserIndex(username) {
193 // Find index for requested user, if it exists
194 for (var i = 0; i < LightDMService.users.count; i++) {
195 if (username === LightDMService.users.data(i, LightDMService.userRoles.NameRole)) {
203 function selectUser(index) {
204 if (index < 0 || index >= LightDMService.users.count)
206 currentIndex = index;
207 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
208 AccountsService.user = user;
209 LauncherModel.setUser(user);
210 LightDMService.greeter.authenticate(user); // always resets auth state
213 function hideView() {
215 loader.item.enabled = false; // drop OSK and prevent interaction
221 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
224 } else if (loader.item) {
225 loader.item.notifyAuthenticationFailed();
229 function startUnlock(toTheRight) {
231 return loader.item.tryToUnlock(toTheRight);
237 function checkForcedUnlock(hideNow) {
238 if (forcedUnlock && shown) {
241 ShellNotifier.greeter.hide(true); // skip hide animation
246 function showFingerprintMessage(msg) {
247 d.selectUser(d.currentIndex);
248 LightDMService.prompts.prepend(msg, LightDMService.prompts.Error);
250 loader.item.showErrorMessage(msg);
251 loader.item.notifyAuthenticationFailed();
256 onLauncherOffsetChanged: {
257 if (launcherOffset > 0) {
258 d.lastKnownPositiveOffset = launcherOffset;
262 onForcedUnlockChanged: d.checkForcedUnlock(false /* hideNow */)
263 Component.onCompleted: d.checkForcedUnlock(true /* hideNow */)
267 AccountsService.failedLogins = 0;
268 AccountsService.failedFingerprintLogins = 0;
270 // Stop delay timer if they logged in with fingerprint
271 forcedDelayTimer.stop();
272 forcedDelayTimer.delayMinutes = 0;
284 schema.id: "com.lomiri.Shell.Greeter"
290 // We use a short interval and check against the system wall clock
291 // because we have to consider the case that the system is suspended
292 // for a few minutes. When we wake up, we want to quickly be correct.
295 property var delayTarget
296 property int delayMinutes
298 function forceDelay() {
299 // Store the beginning time for a lockout in GSettings, so that
300 // we still lock the user out if they reboot. And we store
301 // starting time rather than end-time or how-long because:
302 // - If storing end-time and on boot we have a problem with NTP,
303 // we might get locked out for a lot longer than we thought.
304 // - If storing how-long, and user turns their phone off for an
305 // hour rather than wait, they wouldn't expect to still be locked
307 // - A malicious actor could manipulate either of the above
308 // settings to keep the user out longer. But by storing
309 // start-time, we never make the user wait longer than the full
311 greeterSettings.lockedOutTime = new Date().getTime();
312 checkForForcedDelay();
316 var diff = delayTarget - new Date();
318 delayMinutes = Math.ceil(diff / 60000);
325 function checkForForcedDelay() {
326 if (greeterSettings.lockedOutTime === 0) {
330 var now = new Date();
331 delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
333 // If tooEarly is true, something went very wrong. Bug or NTP
334 // misconfiguration maybe?
335 var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
336 var tooLate = now >= delayTarget;
338 // Compare stored time to system time. If a malicious actor is
339 // able to manipulate time to avoid our lockout, they already have
340 // enough access to cause damage. So we choose to trust this check.
341 if (tooEarly || tooLate) {
349 Component.onCompleted: checkForForcedDelay()
353 // Nothing should leak to items behind the greeter
354 MouseArea { anchors.fill: parent; hoverEnabled: true }
362 active: root.required
363 source: root.viewSource.toString() ? root.viewSource : "GreeterView.qml"
367 item.forceActiveFocus();
368 d.selectUser(d.currentIndex);
369 LightDMService.infographic.readyForDataChange();
379 LightDMService.greeter.respond(response);
384 onTease: root.tease()
385 onEmergencyCall: root.emergencyCall()
387 if (!loader.item.required) {
388 ShellNotifier.greeter.hide(false);
395 property: "panelHeight"
396 value: root.panelHeight
401 property: "launcherOffset"
402 value: d.launcherOffsetProxy
407 property: "dragHandleLeftMargin"
408 value: root.dragHandleLeftMargin
413 property: "delayMinutes"
414 value: forcedDelayTimer.delayMinutes
419 property: "background"
420 value: root.background
425 property: "backgroundSourceSize"
426 value: root.backgroundSourceSize
431 property: "hasCustomBackground"
432 value: root.hasCustomBackground
449 property: "alphanumeric"
450 value: d.alphanumeric
455 property: "currentIndex"
456 value: d.currentIndex
461 property: "userModel"
462 value: LightDMService.users
467 property: "infographicModel"
468 value: LightDMService.infographic
473 property: "inputMethodRect"
474 value: root.inputMethodRect
479 property: "hasKeyboard"
480 value: root.hasKeyboard
485 property: "usageMode"
486 value: root.usageMode
491 property: "multiUser"
497 property: "orientation"
498 value: root.orientation
503 target: LightDMService.greeter
505 onShowGreeter: root.forceShow()
506 onHideGreeter: root.forcedUnlock = true
513 loader.item.notifyAuthenticationFailed();
516 AccountsService.failedLogins++;
518 // Check if we should initiate a forced login delay
519 if (failedLoginsDelayAttempts > 0
520 && AccountsService.failedLogins > 0
521 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
522 forcedDelayTimer.forceDelay();
525 d.selectUser(d.currentIndex);
535 onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user))
539 target: ShellNotifier.greeter
542 root.hideNow(); // skip hide animation
550 target: ShellNotifier.greeter
556 target: DBusLomiriSessionService
557 onLockRequested: root.forceShow()
559 root.forcedUnlock = true;
560 ShellNotifier.greeter.hide(true);
565 target: LightDMService.greeter
571 target: LightDMService.infographic
573 value: AccountsService.statsWelcomeScreen ? LightDMService.users.data(d.currentIndex, LightDMService.userRoles.NameRole) : ""
578 onLanguageChanged: LightDMService.infographic.readyForDataChange()
585 onTriggered: biometryd.startOperation()
586 interval: failedFingerprintReaderRetryDelay
591 objectName: "biometryd"
593 property var operation: null
594 readonly property bool idEnabled: root.active &&
595 root.allowFingerprint &&
596 Powerd.status === Powerd.On &&
597 Biometryd.available &&
598 AccountsService.enableFingerprintIdentification
600 function startOperation() {
602 var identifier = Biometryd.defaultDevice.identifier;
603 operation = identifier.identifyUser();
604 operation.start(biometryd);
608 function cancelOperation() {
615 function restartOperation() {
617 if (failedFingerprintReaderRetryDelay > 0) {
618 fpRetryTimer.running = true;
624 function failOperation(reason) {
625 console.log("Failed to identify user by fingerprint:", reason);
627 var msg = d.secureFingerprint ? i18n.tr("Try again") :
628 d.alphanumeric ? i18n.tr("Enter passphrase to unlock") :
629 i18n.tr("Enter passcode to unlock");
630 d.showFingerprintMessage(msg);
633 Component.onCompleted: startOperation()
634 Component.onDestruction: cancelOperation()
635 onIdEnabledChanged: restartOperation()
638 if (!d.secureFingerprint) {
639 failOperation("fingerprint reader is locked");
642 if (result !== LightDMService.users.data(d.currentIndex, LightDMService.userRoles.UidRole)) {
643 AccountsService.failedFingerprintLogins++;
644 failOperation("not the selected user");
647 console.log("Identified user by fingerprint:", result);
649 loader.item.showFakePassword();
652 root.forcedUnlock = true;
655 if (!d.secureFingerprint) {
656 failOperation("fingerprint reader is locked");
657 } else if (reason !== "ERROR_CANCELED") {
658 AccountsService.failedFingerprintLogins++;
659 failOperation(reason);