Unity 8
Greeter.qml
1 /*
2  * Copyright (C) 2013,2014,2015 Canonical, Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 3.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 import QtQuick 2.3
18 import AccountsService 0.1
19 import LightDM 0.1 as LightDM
20 import Ubuntu.Components 1.1
21 import Ubuntu.SystemImage 0.1
22 import Unity.Launcher 0.1
23 import "../Components"
24 
25 Showable {
26  id: root
27  created: loader.status == Loader.Ready
28 
29  property real dragHandleLeftMargin: 0
30 
31  property url background
32 
33  // How far to offset the top greeter layer during a launcher left-drag
34  property real launcherOffset
35 
36  readonly property bool active: shown || hasLockedApp
37  readonly property bool fullyShown: loader.item ? loader.item.fullyShown : false
38 
39  // True when the greeter is waiting for PAM or other setup process
40  readonly property alias waiting: d.waiting
41 
42  property string lockedApp: ""
43  readonly property bool hasLockedApp: lockedApp !== ""
44 
45  property bool forcedUnlock
46  readonly property bool locked: LightDM.Greeter.active && !LightDM.Greeter.authenticated && !forcedUnlock
47 
48  property bool tabletMode
49  property url viewSource // only used for testing
50 
51  property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
52  property int failedLoginsDelayAttempts: 7 // number of failed logins
53  property real failedLoginsDelayMinutes: 5 // minutes of forced waiting
54 
55  readonly property bool animating: loader.item ? loader.item.animating : false
56 
57  signal tease()
58  signal sessionStarted()
59  signal emergencyCall()
60 
61  function forceShow() {
62  showNow();
63  loader.item.reset();
64  }
65 
66  function notifyAppFocused(appId) {
67  if (!active) {
68  return;
69  }
70 
71  if (hasLockedApp) {
72  if (appId === lockedApp) {
73  hide(); // show locked app
74  } else {
75  show();
76  d.startUnlock(false /* toTheRight */);
77  }
78  } else if (appId !== "unity8-dash") { // dash isn't started by user
79  d.startUnlock(false /* toTheRight */);
80  }
81  }
82 
83  function notifyAboutToFocusApp(appId) {
84  if (!active) {
85  return;
86  }
87 
88  // A hint that we're about to focus an app. This way we can look
89  // a little more responsive, rather than waiting for the above
90  // notifyAppFocused call. We also need this in case we have a locked
91  // app, in order to show lockscreen instead of new app.
92  d.startUnlock(false /* toTheRight */);
93  }
94 
95  // This is a just a glorified notifyAboutToFocusApp(), but it does one
96  // other thing: it hides any cover pages to the RIGHT, because the user
97  // just came from a launcher drag starting on the left.
98  // It also returns a boolean value, indicating whether there was a visual
99  // change or not (the shell only wants to hide the launcher if there was
100  // a change).
101  function notifyShowingDashFromDrag() {
102  if (!active) {
103  return false;
104  }
105 
106  return d.startUnlock(true /* toTheRight */);
107  }
108 
109  QtObject {
110  id: d
111 
112  readonly property bool multiUser: LightDM.Users.count > 1
113  property int currentIndex
114  property bool waiting
115 
116  // We want 'launcherOffset' to animate down to zero. But not to animate
117  // while being dragged. So ideally we change this only when the user
118  // lets go and launcherOffset drops to zero. But we need to wait for
119  // the behavior to be enabled first. So we cache the last known good
120  // launcherOffset value to cover us during that brief gap between
121  // release and the behavior turning on.
122  property real lastKnownPositiveOffset // set in a launcherOffsetChanged below
123  property real launcherOffsetProxy: (shown && !launcherOffsetProxyBehavior.enabled) ? lastKnownPositiveOffset : 0
124  Behavior on launcherOffsetProxy {
125  id: launcherOffsetProxyBehavior
126  enabled: launcherOffset === 0
127  UbuntuNumberAnimation {}
128  }
129 
130  function selectUser(uid, reset) {
131  d.waiting = true;
132  if (reset) {
133  loader.item.reset();
134  }
135  currentIndex = uid;
136  var user = LightDM.Users.data(uid, LightDM.UserRoles.NameRole);
137  AccountsService.user = user;
138  LauncherModel.setUser(user);
139  LightDM.Greeter.authenticate(user); // always resets auth state
140  }
141 
142  function login() {
143  enabled = false;
144  if (LightDM.Greeter.startSessionSync()) {
145  sessionStarted();
146  if (loader.item) {
147  loader.item.notifyAuthenticationSucceeded();
148  }
149  } else if (loader.item) {
150  loader.item.notifyAuthenticationFailed();
151  }
152  enabled = true;
153  }
154 
155  function startUnlock(toTheRight) {
156  if (loader.item) {
157  return loader.item.tryToUnlock(toTheRight);
158  } else {
159  return false;
160  }
161  }
162  }
163 
164  onLauncherOffsetChanged: {
165  if (launcherOffset > 0) {
166  d.lastKnownPositiveOffset = launcherOffset;
167  }
168  }
169 
170  onForcedUnlockChanged: {
171  if (forcedUnlock && shown) {
172  // pretend we were just authenticated
173  loader.item.notifyAuthenticationSucceeded();
174  }
175  }
176 
177  onRequiredChanged: {
178  if (required) {
179  d.waiting = true;
180  lockedApp = "";
181  }
182  }
183 
184  Timer {
185  id: forcedDelayTimer
186 
187  // We use a short interval and check against the system wall clock
188  // because we have to consider the case that the system is suspended
189  // for a few minutes. When we wake up, we want to quickly be correct.
190  interval: 500
191 
192  property var delayTarget;
193  property int delayMinutes;
194 
195  function forceDelay(delay /* in minutes */) {
196  delayTarget = new Date();
197  delayTarget.setTime(delayTarget.getTime() + delay * 60000);
198  delayMinutes = Math.ceil(delay);
199  start();
200  }
201 
202  onTriggered: {
203  var diff = delayTarget - new Date();
204  if (diff > 0) {
205  delayMinutes = Math.ceil(diff / 60000);
206  start(); // go again
207  } else {
208  delayMinutes = 0;
209  }
210  }
211  }
212 
213  // event eater
214  // Nothing should leak to items behind the greeter
215  MouseArea { anchors.fill: parent }
216 
217  Loader {
218  id: loader
219  objectName: "loader"
220 
221  anchors.fill: parent
222 
223  active: root.required
224  source: root.viewSource.toString() ? root.viewSource :
225  (d.multiUser || root.tabletMode) ? "WideView.qml" : "NarrowView.qml"
226 
227  onLoaded: {
228  root.lockedApp = "";
229  root.forceActiveFocus();
230  d.selectUser(d.currentIndex, true);
231  LightDM.Infographic.readyForDataChange();
232  }
233 
234  Connections {
235  target: loader.item
236  onSelected: {
237  d.selectUser(index, true);
238  }
239  onResponded: {
240  if (root.locked) {
241  LightDM.Greeter.respond(response);
242  } else {
243  if (LightDM.Greeter.active && !LightDM.Greeter.authenticated) { // could happen if forcedUnlock
244  d.login();
245  }
246  loader.item.hide();
247  }
248  }
249  onTease: root.tease()
250  onEmergencyCall: root.emergencyCall()
251  onRequiredChanged: {
252  if (!loader.item.required) {
253  root.hide();
254  }
255  }
256  }
257 
258  Binding {
259  target: loader.item
260  property: "backgroundTopMargin"
261  value: -root.y
262  }
263 
264  Binding {
265  target: loader.item
266  property: "launcherOffset"
267  value: d.launcherOffsetProxy
268  }
269 
270  Binding {
271  target: loader.item
272  property: "dragHandleLeftMargin"
273  value: root.dragHandleLeftMargin
274  }
275 
276  Binding {
277  target: loader.item
278  property: "delayMinutes"
279  value: forcedDelayTimer.delayMinutes
280  }
281 
282  Binding {
283  target: loader.item
284  property: "background"
285  value: root.background
286  }
287 
288  Binding {
289  target: loader.item
290  property: "locked"
291  value: root.locked
292  }
293 
294  Binding {
295  target: loader.item
296  property: "alphanumeric"
297  value: AccountsService.passwordDisplayHint === AccountsService.Keyboard
298  }
299 
300  Binding {
301  target: loader.item
302  property: "currentIndex"
303  value: d.currentIndex
304  }
305 
306  Binding {
307  target: loader.item
308  property: "userModel"
309  value: LightDM.Users
310  }
311 
312  Binding {
313  target: loader.item
314  property: "infographicModel"
315  value: LightDM.Infographic
316  }
317  }
318 
319  Connections {
320  target: LightDM.Greeter
321 
322  onShowGreeter: root.forceShow()
323 
324  onHideGreeter: {
325  d.login();
326  loader.item.hide();
327  }
328 
329  onShowMessage: {
330  if (!LightDM.Greeter.active) {
331  return; // could happen if hideGreeter() comes in before we prompt
332  }
333 
334  // inefficient, but we only rarely deal with messages
335  var html = text.replace(/&/g, "&amp;")
336  .replace(/</g, "&lt;")
337  .replace(/>/g, "&gt;")
338  .replace(/\n/g, "<br>");
339  if (isError) {
340  html = "<font color=\"#df382c\">" + html + "</font>";
341  }
342 
343  loader.item.showMessage(html);
344  }
345 
346  onShowPrompt: {
347  d.waiting = false;
348 
349  if (!LightDM.Greeter.active) {
350  return; // could happen if hideGreeter() comes in before we prompt
351  }
352 
353  loader.item.showPrompt(text, isSecret, isDefaultPrompt);
354  }
355 
356  onAuthenticationComplete: {
357  d.waiting = false;
358 
359  if (LightDM.Greeter.authenticated) {
360  AccountsService.failedLogins = 0;
361  d.login();
362  if (!LightDM.Greeter.promptless) {
363  loader.item.hide();
364  }
365  } else {
366  if (!LightDM.Greeter.promptless) {
367  AccountsService.failedLogins++;
368  }
369 
370  // Check if we should initiate a factory reset
371  if (maxFailedLogins >= 2) { // require at least a warning
372  if (AccountsService.failedLogins === maxFailedLogins - 1) {
373  loader.item.showLastChance();
374  } else if (AccountsService.failedLogins >= maxFailedLogins) {
375  SystemImage.factoryReset(); // Ouch!
376  }
377  }
378 
379  // Check if we should initiate a forced login delay
380  if (failedLoginsDelayAttempts > 0
381  && AccountsService.failedLogins > 0
382  && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
383  forcedDelayTimer.forceDelay(failedLoginsDelayMinutes);
384  }
385 
386  loader.item.notifyAuthenticationFailed();
387  if (!LightDM.Greeter.promptless) {
388  d.selectUser(d.currentIndex, false);
389  }
390  }
391  }
392 
393  onRequestAuthenticationUser: {
394  // Find index for requested user, if it exists
395  for (var i = 0; i < LightDM.Users.count; i++) {
396  if (user === LightDM.Users.data(i, LightDM.UserRoles.NameRole)) {
397  d.selectUser(i, true);
398  return;
399  }
400  }
401  }
402  }
403 
404  Binding {
405  target: LightDM.Greeter
406  property: "active"
407  value: root.active
408  }
409 
410  Binding {
411  target: LightDM.Infographic
412  property: "username"
413  value: AccountsService.statsWelcomeScreen ? LightDM.Users.data(d.currentIndex, LightDM.UserRoles.NameRole) : ""
414  }
415 
416  Connections {
417  target: i18n
418  onLanguageChanged: LightDM.Infographic.readyForDataChange()
419  }
420 }