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