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/>.
20import AccountsService 0.1
24import Lomiri.Components 1.3
25import Lomiri.Launcher 0.1
26import Lomiri.Session 0.1
34 created: loader.status == Loader.Ready
36 property real dragHandleLeftMargin: 0
38 property url background
39 property bool hasCustomBackground
40 property real backgroundSourceSize
42 // How far to offset the top greeter layer during a launcher left-drag
43 property real launcherOffset
45 // How far down to position the greeter's interface to avoid the Panel
46 property real panelHeight
48 readonly property bool active: required || hasLockedApp
49 readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
51 property bool allowFingerprint: true
53 // True when the greeter is waiting for PAM or other setup process
54 readonly property alias waiting: d.waiting
56 property string lockedApp: ""
57 readonly property bool hasLockedApp: lockedApp !== ""
59 property bool forcedUnlock
60 readonly property bool locked: LightDMService.greeter.active && !LightDMService.greeter.authenticated && !forcedUnlock
62 property bool tabletMode
63 property string usageMode
64 property url viewSource // only used for testing
66 property int failedLoginsDelayAttempts: 7 // number of failed logins
67 property real failedLoginsDelayMinutes: 5 // minutes of forced waiting
68 property int failedFingerprintLoginsDisableAttempts: 5 // number of failed fingerprint logins
69 property int failedFingerprintReaderRetryDelay: 250 // time to wait before retrying a failed fingerprint read [ms]
71 readonly property bool animating: loader.item ? loader.item.animating : false
73 property rect inputMethodRect
75 property bool hasKeyboard: false
76 property int orientation
79 signal sessionStarted()
80 signal emergencyCall()
82 function forceShow() {
84 d.isLockscreen = true;
89 loader.item.forceShow();
91 // Normally loader.onLoaded will select a user, but if we're
92 // already shown, do it manually.
93 d.selectUser(d.currentIndex);
96 // Even though we may already be shown, we want to call show() for its
97 // possible side effects, like hiding indicators and such.
99 // We re-check forcedUnlock here, because selectUser above might
100 // process events during authentication, and a request to unlock could
101 // have come in in the meantime.
107 function notifyAppFocusRequested(appId) {
113 if (appId === lockedApp) {
114 hide(); // show locked app
117 d.startUnlock(false /* toTheRight */);
120 d.startUnlock(false /* toTheRight */);
124 // Notify that the user has explicitly requested an app
125 function notifyUserRequestedApp() {
130 // A hint that we're about to focus an app. This way we can look
131 // a little more responsive, rather than waiting for the above
132 // notifyAppFocusRequested call. We also need this in case we have a locked
133 // app, in order to show lockscreen instead of new app.
134 d.startUnlock(false /* toTheRight */);
137 // This is a just a glorified notifyUserRequestedApp(), but it does one
138 // other thing: it hides any cover pages to the RIGHT, because the user
139 // just came from a launcher drag starting on the left.
140 // It also returns a boolean value, indicating whether there was a visual
141 // change or not (the shell only wants to hide the launcher if there was
143 function notifyShowingDashFromDrag() {
148 return d.startUnlock(true /* toTheRight */);
151 function sessionToStart() {
152 for (var i = 0; i < LightDMService.sessions.count; i++) {
153 var session = LightDMService.sessions.data(i,
154 LightDMService.sessionRoles.KeyRole);
155 if (loader.item.sessionToStart === session) {
160 return LightDMService.greeter.defaultSession;
166 readonly property bool multiUser: LightDMService.users.count > 1
167 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
168 property int currentIndex: Math.max(selectUserIndex, 0)
169 readonly property bool waiting: LightDMService.prompts.count == 0 && !root.forcedUnlock
170 property bool isLockscreen // true when we are locking an active session, rather than first user login
171 readonly property bool secureFingerprint: isLockscreen &&
172 AccountsService.failedFingerprintLogins <
173 root.failedFingerprintLoginsDisableAttempts
174 readonly property bool alphanumeric: AccountsService.passwordDisplayHint === AccountsService.Keyboard
176 // We want 'launcherOffset' to animate down to zero. But not to animate
177 // while being dragged. So ideally we change this only when the user
178 // lets go and launcherOffset drops to zero. But we need to wait for
179 // the behavior to be enabled first. So we cache the last known good
180 // launcherOffset value to cover us during that brief gap between
181 // release and the behavior turning on.
182 property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
183 property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
184 Behavior on launcherOffsetProxy {
185 id: launcherOffsetProxyBehavior
186 enabled: launcherOffset === 0
187 LomiriNumberAnimation {}
190 function getUserIndex(username) {
194 // Find index for requested user, if it exists
195 for (var i = 0; i < LightDMService.users.count; i++) {
196 if (username === LightDMService.users.data(i, LightDMService.userRoles.NameRole)) {
204 function selectUser(index) {
205 if (index < 0 || index >= LightDMService.users.count)
207 currentIndex = index;
208 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
209 AccountsService.user = user;
210 LauncherModel.setUser(user);
211 LightDMService.greeter.authenticate(user); // always resets auth state
214 function hideView() {
216 loader.item.enabled = false; // drop OSK and prevent interaction
222 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
225 } else if (loader.item) {
226 loader.item.notifyAuthenticationFailed();
230 function startUnlock(toTheRight) {
232 return loader.item.tryToUnlock(toTheRight);
238 function checkForcedUnlock(hideNow) {
239 if (forcedUnlock && shown) {
242 ShellNotifier.greeter.hide(true); // skip hide animation
247 function showFingerprintMessage(msg) {
248 d.selectUser(d.currentIndex);
249 LightDMService.prompts.prepend(msg, LightDMService.prompts.Error);
251 loader.item.showErrorMessage(msg);
252 loader.item.notifyAuthenticationFailed();
257 onLauncherOffsetChanged: {
258 if (launcherOffset > 0) {
259 d.lastKnownPositiveOffset = launcherOffset;
263 onForcedUnlockChanged: d.checkForcedUnlock(false /* hideNow */)
264 Component.onCompleted: d.checkForcedUnlock(true /* hideNow */)
268 AccountsService.failedLogins = 0;
269 AccountsService.failedFingerprintLogins = 0;
271 // Stop delay timer if they logged in with fingerprint
272 forcedDelayTimer.stop();
273 forcedDelayTimer.delayMinutes = 0;
285 schema.id: "com.lomiri.Shell.Greeter"
291 // We use a short interval and check against the system wall clock
292 // because we have to consider the case that the system is suspended
293 // for a few minutes. When we wake up, we want to quickly be correct.
296 property var delayTarget
297 property int delayMinutes
299 function forceDelay() {
300 // Store the beginning time for a lockout in GSettings, so that
301 // we still lock the user out if they reboot. And we store
302 // starting time rather than end-time or how-long because:
303 // - If storing end-time and on boot we have a problem with NTP,
304 // we might get locked out for a lot longer than we thought.
305 // - If storing how-long, and user turns their phone off for an
306 // hour rather than wait, they wouldn't expect to still be locked
308 // - A malicious actor could manipulate either of the above
309 // settings to keep the user out longer. But by storing
310 // start-time, we never make the user wait longer than the full
312 greeterSettings.lockedOutTime = new Date().getTime();
313 checkForForcedDelay();
317 var diff = delayTarget - new Date();
319 delayMinutes = Math.ceil(diff / 60000);
326 function checkForForcedDelay() {
327 if (greeterSettings.lockedOutTime === 0) {
331 var now = new Date();
332 delayTarget = new Date(greeterSettings.lockedOutTime + failedLoginsDelayMinutes * 60000);
334 // If tooEarly is true, something went very wrong. Bug or NTP
335 // misconfiguration maybe?
336 var tooEarly = now.getTime() < greeterSettings.lockedOutTime;
337 var tooLate = now >= delayTarget;
339 // Compare stored time to system time. If a malicious actor is
340 // able to manipulate time to avoid our lockout, they already have
341 // enough access to cause damage. So we choose to trust this check.
342 if (tooEarly || tooLate) {
350 Component.onCompleted: checkForForcedDelay()
354 // Nothing should leak to items behind the greeter
355 MouseArea { anchors.fill: parent; hoverEnabled: true }
363 active: root.required
364 source: root.viewSource.toString() ? root.viewSource : "GreeterView.qml"
368 item.forceActiveFocus();
369 d.selectUser(d.currentIndex);
370 LightDMService.infographic.readyForDataChange();
375 function onSelected(index) {
378 function onResponded(response) {
380 LightDMService.greeter.respond(response);
385 function onTease() { root.tease() }
386 function onEmergencyCall() { root.emergencyCall() }
387 function onRequiredChanged() {
388 if (!loader.item.required) {
389 ShellNotifier.greeter.hide(false);
396 property: "panelHeight"
397 value: root.panelHeight
398 restoreMode: Binding.RestoreBinding
403 property: "launcherOffset"
404 value: d.launcherOffsetProxy
405 restoreMode: Binding.RestoreBinding
410 property: "dragHandleLeftMargin"
411 value: root.dragHandleLeftMargin
412 restoreMode: Binding.RestoreBinding
417 property: "delayMinutes"
418 value: forcedDelayTimer.delayMinutes
419 restoreMode: Binding.RestoreBinding
424 property: "background"
425 value: root.background
426 restoreMode: Binding.RestoreBinding
431 property: "backgroundSourceSize"
432 value: root.backgroundSourceSize
433 restoreMode: Binding.RestoreBinding
438 property: "hasCustomBackground"
439 value: root.hasCustomBackground
440 restoreMode: Binding.RestoreBinding
447 restoreMode: Binding.RestoreBinding
454 restoreMode: Binding.RestoreBinding
459 property: "alphanumeric"
460 value: d.alphanumeric
465 property: "currentIndex"
466 value: d.currentIndex
471 property: "userModel"
472 value: LightDMService.users
477 property: "infographicModel"
478 value: LightDMService.infographic
483 property: "inputMethodRect"
484 value: root.inputMethodRect
489 property: "hasKeyboard"
490 value: root.hasKeyboard
495 property: "usageMode"
496 value: root.usageMode
501 property: "multiUser"
507 property: "orientation"
508 value: root.orientation
513 target: LightDMService.greeter
515 function onShowGreeter() { root.forceShow() }
516 function onHideGreeter() { root.forcedUnlock = true }
518 function onLoginError(automatic) {
523 loader.item.notifyAuthenticationFailed();
526 AccountsService.failedLogins++;
528 // Check if we should initiate a forced login delay
529 if (failedLoginsDelayAttempts > 0
530 && AccountsService.failedLogins > 0
531 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
532 forcedDelayTimer.forceDelay();
535 d.selectUser(d.currentIndex);
539 function onLoginSuccess(automatic) {
545 function onRequestAuthenticationUser(user) { d.selectUser(d.getUserIndex(user)) }
549 target: ShellNotifier.greeter
550 function onHide(now) {
552 root.hideNow(); // skip hide animation
560 target: ShellNotifier.greeter
566 target: DBusLomiriSessionService
567 function onLockRequested() { root.forceShow() }
568 function onUnlocked() {
569 root.forcedUnlock = true;
570 ShellNotifier.greeter.hide(true);
575 target: LightDMService.greeter
581 target: LightDMService.infographic
583 value: AccountsService.statsWelcomeScreen ? LightDMService.users.data(d.currentIndex, LightDMService.userRoles.NameRole) : ""
588 function onLanguageChanged() { LightDMService.infographic.readyForDataChange() }
595 onTriggered: biometryd.startOperation()
596 interval: failedFingerprintReaderRetryDelay
601 objectName: "biometryd"
603 property var operation: null
604 readonly property bool idEnabled: root.active &&
605 root.allowFingerprint &&
606 Powerd.status === Powerd.On &&
607 Biometryd.available &&
608 AccountsService.enableFingerprintIdentification
610 function startOperation() {
612 var identifier = Biometryd.defaultDevice.identifier;
613 operation = identifier.identifyUser();
614 operation.start(biometryd);
618 function cancelOperation() {
625 function restartOperation() {
627 if (failedFingerprintReaderRetryDelay > 0) {
628 fpRetryTimer.running = true;
634 function failOperation(reason) {
635 console.log("Failed to identify user by fingerprint:", reason);
637 var msg = d.secureFingerprint ? i18n.tr("Try again") :
638 d.alphanumeric ? i18n.tr("Enter passphrase to unlock") :
639 i18n.tr("Enter passcode to unlock");
640 d.showFingerprintMessage(msg);
643 Component.onCompleted: startOperation()
644 Component.onDestruction: cancelOperation()
645 onIdEnabledChanged: restartOperation()
648 if (!d.secureFingerprint) {
649 failOperation("fingerprint reader is locked");
652 if (result !== LightDMService.users.data(d.currentIndex, LightDMService.userRoles.UidRole)) {
653 AccountsService.failedFingerprintLogins++;
654 failOperation("not the selected user");
657 console.log("Identified user by fingerprint:", result);
659 loader.item.showFakePassword();
662 root.forcedUnlock = true;
665 if (!d.secureFingerprint) {
666 failOperation("fingerprint reader is locked");
667 } else if (reason !== "ERROR_CANCELED") {
668 AccountsService.failedFingerprintLogins++;
669 failOperation(reason);