on xenial help sign in
Fix some security issues with the tablet greeter, which allowed the lockscreen to be bypassed. (LP: #1367715)

Recent greeter fixes for phone mode did not fully take the tablet scenario into account. This branch does three main things: 1) Rename fakeActiveForApp because it is nothing but confusing. I renamed the variable itself to lockedApp and added a new variable hasLockedApp, which is infinitely clearer than fakeActiveForApp !== "", which is what we used to use. Since that is a very security-sensitive variable, I figured it's best to be clear. 2) Closed a security bug where if the user could plug in a phone showing the emergency dialer to a bigger screen, thus switching to tablet mode, they could get access to other apps. 3) Closed a security bug where the tablet lockscreen could be left-swiped away. 4) Closed a security bug where the tablet would unlock itself if you launched an app from the indicators or launcher. Now it just focuses the prompt field (much like how the phone shows the lockscreen pin pad). Fixes: 1367715 Approved by: Michael Zanetti

commit5e81d15737510495985e301680a09dd8d8f0fcf0 authorMichael Terry <michael.terry@canonical.com> dateThu 9 Oct 2014, 14:05 (12 years ago) parentcc5a1a795d5403
9 files changed+517−74
qml/Greeter/Greeter.qml+13−1 qml/Greeter/GreeterContent.qml+12 qml/Greeter/LoginList.qml+19−6 qml/Shell.qml+74−40 tests/qmltests/CMakeLists.txt+1 tests/qmltests/Greeter/tst_MultiGreeter.qml+2−2 tests/qmltests/tst_Shell.qml+1 tests/qmltests/tst_ShellWithPin.qml+106−25 tests/qmltests/tst_TabletShell.qml+289
qml/Greeter/Greeter.qml+13−1
@@ -36,7 +36,7 @@ Showable {
3636
3737 property alias dragHandleWidth: dragHandle.width
3838 property alias model: greeterContentLoader.model
39− property bool locked: shown && !LightDM.Greeter.promptless
39+ property bool locked: true
4040
4141 readonly property bool narrowMode: !multiUser && height > width
4242 readonly property bool multiUser: LightDM.Users.count > 1
@@ -57,6 +57,18 @@ Showable {
5757 }
5858 }
5959
60+ function tryToUnlock() {
61+ if (created) {
62+ greeterContentLoader.item.tryToUnlock()
63+ }
64+ }
65+
66+ function reset() {
67+ if (created) {
68+ greeterContentLoader.item.reset()
69+ }
70+ }
71+
6072 onRequiredChanged: {
6173 // Reset hide animation to default once we're finished with it
6274 if (required) {
qml/Greeter/GreeterContent.qml+12
@@ -29,6 +29,18 @@ Item {
2929 signal selected(int uid)
3030 signal unlocked(int uid)
3131
32+ function tryToUnlock() {
33+ if (loginLoader.item) {
34+ loginLoader.item.tryToUnlock()
35+ }
36+ }
37+
38+ function reset() {
39+ if (loginLoader.item) {
40+ loginLoader.item.reset()
41+ }
42+ }
43+
3244 Rectangle {
3345 // In case background fails to load
3446 id: backgroundBackup
qml/Greeter/LoginList.qml+19−6
@@ -35,6 +35,22 @@ Item {
3535 signal selected(int uid)
3636 signal unlocked(int uid)
3737
38+ function tryToUnlock() {
39+ if (LightDM.Greeter.promptless) {
40+ if (LightDM.Greeter.authenticated) {
41+ root.unlocked(userList.currentIndex)
42+ } else {
43+ root.resetAuthentication()
44+ }
45+ } else {
46+ passwordInput.forceActiveFocus()
47+ }
48+ }
49+
50+ function reset() {
51+ root.resetAuthentication()
52+ }
53+
3854 Keys.onEscapePressed: root.resetAuthentication()
3955
4056 Rectangle {
@@ -229,14 +245,11 @@ Item {
229245 }
230246
231247 MouseArea {
248+ id: passwordMouseArea
249+ objectName: "passwordMouseArea"
232250 anchors.fill: passwordInput
233251 enabled: LightDM.Greeter.promptless
234− onClicked: {
235− if (LightDM.Greeter.authenticated)
236− root.unlocked(userList.currentIndex);
237− else
238− root.resetAuthentication();
239− }
252+ onClicked: root.tryToUnlock()
240253 }
241254
242255 function resetAuthentication() {
qml/Shell.qml+74−40
@@ -87,9 +87,9 @@ Item {
8787 }
8888 }
8989
90− function setFakeActiveForApp(app) {
90+ function setLockedApp(app) {
9191 if (shell.locked) {
92− greeter.fakeActiveForApp = app
92+ greeter.lockedApp = app
9393 lockscreen.hide()
9494 }
9595 }
@@ -169,31 +169,37 @@ Item {
169169 Connections {
170170 target: ApplicationManager
171171 onFocusRequested: {
172− if (appId === "dialer-app") {
173− // Always let the dialer-app through. Either user asked
174− // for an emergency call or accepted an incoming call.
175− setFakeActiveForApp(appId)
176− } else if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== appId) {
177− lockscreen.show();
172+ if (greeter.narrowMode) {
173+ if (appId === "dialer-app") {
174+ // Always let the dialer-app through. Either user asked
175+ // for an emergency call or accepted an incoming call.
176+ setLockedApp(appId)
177+ } else if (greeter.hasLockedApp && greeter.lockedApp !== appId) {
178+ greeter.startUnlock()
179+ }
180+ greeter.hide();
181+ } else {
182+ if (LightDM.Greeter.active) {
183+ greeter.startUnlock()
184+ }
178185 }
179− greeter.hide();
180186 }
181187
182188 onFocusedApplicationIdChanged: {
183− if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== ApplicationManager.focusedApplicationId) {
184− lockscreen.show();
189+ if (greeter.hasLockedApp && greeter.lockedApp !== ApplicationManager.focusedApplicationId) {
190+ greeter.startUnlock()
185191 }
186192 panel.indicators.hide();
187193 }
188194
189195 onApplicationAdded: {
190196 if (greeter.shown && appId != "unity8-dash") {
191− greeter.hide();
197+ greeter.startUnlock()
192198 }
193− if (appId === "dialer-app") {
199+ if (greeter.narrowMode && appId === "dialer-app") {
194200 // Always let the dialer-app through. Either user asked
195201 // for an emergency call or accepted an incoming call.
196− setFakeActiveForApp(appId)
202+ setLockedApp(appId)
197203 }
198204 launcher.hide();
199205 }
@@ -201,9 +207,19 @@ Item {
201207
202208 Loader {
203209 id: applicationsDisplayLoader
210+ objectName: "applicationsDisplayLoader"
204211 anchors.fill: parent
205212
206− source: shell.sideStageEnabled ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
213+ // When we have a locked app, we only want to show that one app.
214+ // FIXME: do this in a less traumatic way. We currently only allow
215+ // locked apps in phone mode (see FIXME in Lockscreen component in
216+ // this same file). When that changes, we need to do something
217+ // nicer here. But this code is currently just to prevent a
218+ // theoretical attack where user enters lockedApp mode, then makes
219+ // the screen larger (maybe connects to monitor) and tries to enter
220+ // tablet mode.
221+ property bool tabletMode: shell.sideStageEnabled && !greeter.hasLockedApp
222+ source: tabletMode ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
207223
208224 Binding {
209225 target: applicationsDisplayLoader.item
@@ -229,7 +245,7 @@ Item {
229245 Binding {
230246 target: applicationsDisplayLoader.item
231247 property: "spreadEnabled"
232− value: edgeDemo.stagesEnabled && greeter.fakeActiveForApp === "" // to support emergency dialer hack
248+ value: edgeDemo.stagesEnabled && !greeter.hasLockedApp
233249 }
234250 Binding {
235251 target: applicationsDisplayLoader.item
@@ -304,14 +320,14 @@ Item {
304320 // and wider screens are tablets which don't. When we do allow this
305321 // on devices with a side stage and a SIM, work should be done to
306322 // ensure that the main stage is disabled while the dialer is present
307− // in the side stage.
323+ // in the side stage. See the FIXME in the stage loader in this file.
308324 showEmergencyCallButton: !shell.sideStageEnabled
309325
310326 onEntered: LightDM.Greeter.respond(passphrase);
311327 onCancel: greeter.show()
312− onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter fake-active mode
328+ onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter locked-app mode
313329
314− onShownChanged: if (shown) greeter.fakeActiveForApp = ""
330+ onShownChanged: if (shown) greeter.lockedApp = ""
315331
316332 Timer {
317333 id: forcedDelayTimer
@@ -360,11 +376,13 @@ Item {
360376 }
361377
362378 onPromptlessChanged: {
363− if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
364− lockscreen.hide()
365− } else {
366− lockscreen.reset();
367− lockscreen.show();
379+ if (greeter.narrowMode) {
380+ if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
381+ lockscreen.hide()
382+ } else {
383+ lockscreen.reset();
384+ lockscreen.show();
385+ }
368386 }
369387 }
370388
@@ -414,7 +432,7 @@ Item {
414432 Binding {
415433 target: LightDM.Greeter
416434 property: "active"
417− value: greeter.shown || lockscreen.shown || greeter.fakeActiveForApp != ""
435+ value: greeter.shown || lockscreen.shown || greeter.hasLockedApp
418436 }
419437
420438 Rectangle {
@@ -426,7 +444,8 @@ Item {
426444 Item {
427445 // Just a tiny wrapper to adjust greeter's x without messing with its own dragging
428446 id: greeterWrapper
429− x: launcher.progress
447+ objectName: "greeterWrapper"
448+ x: greeter.narrowMode ? launcher.progress : 0
430449 y: panel.panelHeight
431450 width: parent.width
432451 height: parent.height - panel.panelHeight
@@ -439,7 +458,7 @@ Item {
439458 property bool fullyShown: showProgress === 1.0
440459 onFullyShownChanged: {
441460 // Wait until the greeter is completely covering lockscreen before resetting it.
442− if (fullyShown && !LightDM.Greeter.authenticated) {
461+ if (greeter.narrowMode && fullyShown && !LightDM.Greeter.authenticated) {
443462 lockscreen.reset();
444463 lockscreen.show();
445464 }
@@ -448,7 +467,7 @@ Item {
448467 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
449468 onShowProgressChanged: {
450469 if (showProgress === 0) {
451− if (LightDM.Greeter.authenticated) {
470+ if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
452471 greeter.login()
453472 } else if (greeter.narrowMode) {
454473 lockscreen.clear(false) // to reset focus if necessary
@@ -462,13 +481,16 @@ Item {
462481
463482 signal sessionStarted() // helpful for tests
464483
465− property string fakeActiveForApp: ""
484+ property string lockedApp: ""
485+ property bool hasLockedApp: lockedApp !== ""
466486
467487 available: true
468488 hides: [launcher, panel.indicators]
469489 shown: true
470490 loadContent: required || lockscreen.required // keeps content in memory for quick show()
471491
492+ locked: shell.locked
493+
472494 defaultBackground: shell.background
473495
474496 width: parent.width
@@ -476,6 +498,18 @@ Item {
476498
477499 dragHandleWidth: shell.edgeSize
478500
501+ function startUnlock() {
502+ if (narrowMode) {
503+ if (!LightDM.Greeter.authenticated) {
504+ lockscreen.show()
505+ }
506+ hide()
507+ } else {
508+ show()
509+ tryToUnlock()
510+ }
511+ }
512+
479513 function login() {
480514 enabled = false;
481515 if (LightDM.Greeter.startSessionSync()) {
@@ -491,8 +525,10 @@ Item {
491525 if (shown) {
492526 if (greeter.narrowMode) {
493527 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
528+ } else {
529+ reset()
494530 }
495− greeter.fakeActiveForApp = "";
531+ greeter.lockedApp = "";
496532 greeter.forceActiveFocus();
497533 }
498534 }
@@ -537,10 +573,7 @@ Item {
537573 }
538574
539575 if (LightDM.Greeter.active) {
540− if (!LightDM.Greeter.authenticated) {
541− lockscreen.show()
542− }
543− greeter.hide()
576+ greeter.startUnlock()
544577 }
545578
546579 var animate = !LightDM.Greeter.active && !stages.shown
@@ -549,7 +582,8 @@ Item {
549582 }
550583
551584 function showDash() {
552− if (greeter.fakeActiveForApp !== "") { // just in case user gets here
585+ if (greeter.hasLockedApp || // just in case user gets here
586+ (!greeter.narrowMode && shell.locked)) {
553587 return
554588 }
555589
@@ -576,7 +610,7 @@ Item {
576610 anchors.fill: parent //because this draws indicator menus
577611 indicators {
578612 hides: [launcher]
579− available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && greeter.fakeActiveForApp === ""
613+ available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && !greeter.hasLockedApp
580614 contentEnabled: edgeDemo.panelContentEnabled
581615 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
582616 panelHeight: units.gu(3)
@@ -587,7 +621,7 @@ Item {
587621 ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
588622
589623 fullscreenMode: (topmostApplicationIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
590− || greeter.fakeActiveForApp !== ""
624+ || greeter.hasLockedApp
591625 }
592626
593627 Launcher {
@@ -600,7 +634,7 @@ Item {
600634 anchors.bottom: parent.bottom
601635 width: parent.width
602636 dragAreaWidth: shell.edgeSize
603− available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && greeter.fakeActiveForApp === ""
637+ available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && !greeter.hasLockedApp
604638
605639 onShowDashHome: showHome()
606640 onDash: showDash()
@@ -610,8 +644,8 @@ Item {
610644 }
611645 }
612646 onLauncherApplicationSelected: {
613− if (greeter.fakeActiveForApp !== "") {
614− lockscreen.show()
647+ if (greeter.hasLockedApp) {
648+ greeter.startUnlock()
615649 }
616650 if (!edgeDemo.running)
617651 shell.activateApplication(appId)
tests/qmltests/CMakeLists.txt+1
@@ -21,6 +21,7 @@ set(qmltest_DEFAULT_PROPERTIES ENVIRONMENT "LC_ALL=C")
2121
2222add_qml_test(. Shell ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single")
2323add_qml_test(. ShellWithPin ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single-pin")
24+add_qml_test(. TabletShell ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/full")
2425add_qml_test(Components Background)
2526add_qml_test(Components Carousel)
2627add_qml_test(Components DraggingArea)
tests/qmltests/Greeter/tst_MultiGreeter.qml+2−2
@@ -29,6 +29,7 @@ Item {
2929 Greeter {
3030 id: greeter
3131 anchors.fill: parent
32+ locked: !LightDM.Greeter.authenticated
3233 }
3334
3435 Component {
@@ -115,8 +116,7 @@ Item {
115116 var waitForSignal = data.uid != 0 && userList.currentIndex != data.uid
116117 select_index(data.uid)
117118 tryCompare(userList, "currentIndex", data.uid)
118− tryCompare(greeter, "locked", data.tag !== "no-password" &&
119− data.tag !== "auth-error")
119+ tryCompare(greeter, "locked", data.tag !== "no-password")
120120 if (waitForSignal) {
121121 selectionSpy.wait()
122122 tryCompare(selectionSpy, "count", 1)
tests/qmltests/tst_Shell.qml+1
@@ -484,6 +484,7 @@ Row {
484484 tryCompare(greeter, "showProgress", 0)
485485 waitForRendering(greeter);
486486 LightDM.Greeter.showGreeter()
487+ waitForRendering(greeter)
487488 tryCompare(greeter, "showProgress", 1)
488489 LightDM.Greeter.hideGreeter()
489490 tryCompare(greeter, "showProgress", 0)
tests/qmltests/tst_ShellWithPin.qml+106−25
@@ -27,10 +27,9 @@ import Powerd 0.1
2727
2828import "../../qml"
2929
30−Item {
30+Row {
3131 id: root
32− width: shell.width + units.gu(20)
33− height: shell.height
32+ spacing: 0
3433
3534 QtObject {
3635 id: applicationArguments
@@ -48,21 +47,55 @@ Item {
4847 }
4948 }
5049
51− Shell {
52− id: shell
53− maxFailedLogins: maxRetriesTextField.text
54− }
55− Column {
56− anchors { top: parent.top; right: parent.right; bottom: parent.bottom; margins:units.gu(1) }
57− width: units.gu(18)
50+ Loader {
51+ id: shellLoader
52+
53+ width: units.gu(40)
54+ height: units.gu(71)
5855
59− Label {
60− text: "Max retries:"
61− color: "black"
56+ property bool itemDestroyed: false
57+ sourceComponent: Component {
58+ Shell {
59+ Component.onDestruction: {
60+ shellLoader.itemDestroyed = true
61+ }
62+ maxFailedLogins: maxRetriesTextField.text
63+ }
6264 }
63− TextField {
64− id: maxRetriesTextField
65− text: "-1"
65+ }
66+
67+ Rectangle {
68+ color: "white"
69+ width: units.gu(30)
70+ height: shellLoader.height
71+
72+ Column {
73+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
74+ spacing: units.gu(1)
75+ Row {
76+ anchors { left: parent.left; right: parent.right }
77+ Button {
78+ text: "Show Greeter"
79+ onClicked: {
80+ if (shellLoader.status !== Loader.Ready)
81+ return
82+
83+ var greeter = testCase.findChild(shellLoader.item, "greeter")
84+ if (!greeter.shown) {
85+ greeter.show()
86+ }
87+ }
88+ }
89+ }
90+
91+ Label {
92+ text: "Max retries:"
93+ color: "black"
94+ }
95+ TextField {
96+ id: maxRetriesTextField
97+ text: "-1"
98+ }
6699 }
67100 }
68101
@@ -81,12 +114,10 @@ Item {
81114 name: "ShellWithPin"
82115 when: windowShown
83116
84− function initTestCase() {
85−
86− sessionSpy.target = findChild(shell, "greeter")
87− }
117+ property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
88118
89119 function init() {
120+ sessionSpy.target = findChild(shell, "greeter")
90121 swipeAwayGreeter()
91122 shell.failedLoginsDelayAttempts = -1
92123 maxRetriesTextField.text = "-1"
@@ -95,12 +126,28 @@ Item {
95126 }
96127
97128 function cleanup() {
98− LightDM.Greeter.showGreeter()
99− var greeter = findChild(shell, "greeter")
100− tryCompare(greeter, "showProgress", 1)
129+ shellLoader.itemDestroyed = false
130+
131+ shellLoader.active = false
132+
133+ tryCompare(shellLoader, "status", Loader.Null)
134+ tryCompare(shellLoader, "item", null)
135+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
136+ // item might still be alive. So if we set Loader.active back to true
137+ // again right now we will get the very same Shell instance back. So no reload
138+ // actually took place. Likely because Loader waits until the next event loop
139+ // iteration to do its work. So to ensure the reload, we will wait until the
140+ // Shell instance gets destroyed.
141+ tryCompare(shellLoader, "itemDestroyed", true)
101142
102143 // kill all (fake) running apps
103144 killApps()
145+
146+ // reload our test subject to get it in a fresh state once again
147+ shellLoader.active = true
148+
149+ tryCompare(shellLoader, "status", Loader.Ready)
150+ removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item)
104151 }
105152
106153 function killApps() {
@@ -113,6 +160,7 @@ Item {
113160
114161 function swipeAwayGreeter() {
115162 var greeter = findChild(shell, "greeter");
163+ waitForRendering(greeter)
116164 tryCompare(greeter, "showProgress", 1);
117165
118166 var touchX = shell.width - (shell.edgeSize / 2);
@@ -168,7 +216,8 @@ Item {
168216
169217 mouseClick(emergencyButton, units.gu(1), units.gu(1))
170218
171− tryCompare(greeter, "fakeActiveForApp", "dialer-app")
219+ tryCompare(greeter, "lockedApp", "dialer-app")
220+ tryCompare(greeter, "hasLockedApp", true)
172221 tryCompare(lockscreen, "shown", false)
173222 tryCompare(panel, "fullscreenMode", true)
174223 tryCompare(indicators, "available", false)
@@ -180,7 +229,8 @@ Item {
180229 LightDM.Greeter.showGreeter()
181230
182231 tryCompare(greeter, "shown", true)
183− tryCompare(greeter, "fakeActiveForApp", "")
232+ tryCompare(greeter, "lockedApp", "")
233+ tryCompare(greeter, "hasLockedApp", false)
184234 tryCompare(lockscreen, "shown", true)
185235 tryCompare(panel, "fullscreenMode", false)
186236 tryCompare(indicators, "available", true)
@@ -253,5 +303,36 @@ Item {
253303 enterPin("1111")
254304 tryCompare(resetSpy, "count", 1)
255305 }
306+
307+ function test_emergencyDialerLockOut() {
308+ // This is a theoretical attack on the lockscreen: Enter emergency
309+ // dialer mode on a phone, then plug into a larger screen,
310+ // switching to a tablet interface. This would in theory move the
311+ // dialer to a side stage and give access to other apps. So just
312+ // confirm that such an attack doesn't work.
313+
314+ var applicationsDisplayLoader = findChild(shell, "applicationsDisplayLoader")
315+
316+ // We start in phone mode
317+ tryCompare(shell, "sideStageEnabled", false)
318+ tryCompare(applicationsDisplayLoader, "tabletMode", false)
319+
320+ var app = ApplicationManager.startApplication("dialer-app")
321+
322+ var greeter = findChild(shell, "greeter")
323+ tryCompare(greeter, "showProgress", 0)
324+ tryCompare(greeter, "hasLockedApp", true)
325+
326+ // OK, we're in. Now try (but fail) to switch to tablet mode
327+ shell.tablet = true
328+ tryCompare(shell, "sideStageEnabled", true)
329+ tryCompare(applicationsDisplayLoader, "tabletMode", false)
330+
331+ // And when we kill the app, we go back to locked tablet mode
332+ killApps()
333+ tryCompare(greeter, "showProgress", 1)
334+ tryCompare(shell, "sideStageEnabled", true)
335+ tryCompare(applicationsDisplayLoader, "tabletMode", true)
336+ }
256337 }
257338}
tests/qmltests/tst_TabletShell.qmlnew file+289
@@ -0,0 +1,289 @@
1+/*
2+ * Copyright (C) 2013,2014 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.0
18+import QtTest 1.0
19+import GSettings 1.0
20+import LightDM 0.1 as LightDM
21+import Ubuntu.Components 1.1
22+import Ubuntu.Telephony 0.1 as Telephony
23+import Unity.Application 0.1
24+import Unity.Connectivity 0.1
25+import Unity.Test 0.1 as UT
26+import Powerd 0.1
27+
28+import "../../qml"
29+
30+Row {
31+ id: root
32+ spacing: 0
33+
34+ QtObject {
35+ id: applicationArguments
36+
37+ function hasGeometry() {
38+ return false
39+ }
40+
41+ function width() {
42+ return 0
43+ }
44+
45+ function height() {
46+ return 0
47+ }
48+ }
49+
50+ Loader {
51+ id: shellLoader
52+
53+ width: units.gu(100)
54+ height: units.gu(80)
55+
56+ property bool itemDestroyed: false
57+ sourceComponent: Component {
58+ Shell {
59+ Component.onDestruction: {
60+ shellLoader.itemDestroyed = true
61+ }
62+ }
63+ }
64+ }
65+
66+ Rectangle {
67+ color: "white"
68+ width: units.gu(20)
69+ height: shellLoader.height
70+
71+ Column {
72+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
73+ spacing: units.gu(1)
74+ Row {
75+ anchors { left: parent.left; right: parent.right }
76+ Button {
77+ text: "Show Greeter"
78+ onClicked: {
79+ if (shellLoader.status !== Loader.Ready)
80+ return
81+
82+ var greeter = testCase.findChild(shellLoader.item, "greeter")
83+ if (!greeter.shown) {
84+ greeter.show()
85+ }
86+ }
87+ }
88+ }
89+ }
90+ }
91+
92+ SignalSpy {
93+ id: sessionSpy
94+ signalName: "sessionStarted"
95+ }
96+
97+ SignalSpy {
98+ id: dashCommunicatorSpy
99+ signalName: "setCurrentScopeCalled"
100+ }
101+
102+ SignalSpy {
103+ id: unlockAllModemsSpy
104+ target: Connectivity
105+ signalName: "unlockingAllModems"
106+ }
107+
108+ Telephony.CallEntry {
109+ id: phoneCall
110+ phoneNumber: "+447812221111"
111+ }
112+
113+ UT.UnityTestCase {
114+ id: testCase
115+ name: "TabletShell"
116+ when: windowShown
117+
118+ property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
119+
120+ function init() {
121+ sessionSpy.clear()
122+ sessionSpy.target = findChild(shell, "greeter")
123+ dashCommunicatorSpy.target = findInvisibleChild(shell, "dashCommunicator")
124+ }
125+
126+ function cleanup() {
127+ shellLoader.itemDestroyed = false
128+
129+ shellLoader.active = false
130+
131+ tryCompare(shellLoader, "status", Loader.Null)
132+ tryCompare(shellLoader, "item", null)
133+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
134+ // item might still be alive. So if we set Loader.active back to true
135+ // again right now we will get the very same Shell instance back. So no reload
136+ // actually took place. Likely because Loader waits until the next event loop
137+ // iteration to do its work. So to ensure the reload, we will wait until the
138+ // Shell instance gets destroyed.
139+ tryCompare(shellLoader, "itemDestroyed", true)
140+
141+ // kill all (fake) running apps
142+ killApps()
143+
144+ unlockAllModemsSpy.clear()
145+
146+ // reload our test subject to get it in a fresh state once again
147+ shellLoader.active = true
148+
149+ tryCompare(shellLoader, "status", Loader.Ready)
150+ removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item)
151+ }
152+
153+ function killApps() {
154+ while (ApplicationManager.count > 1) {
155+ var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
156+ ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId)
157+ }
158+ compare(ApplicationManager.count, 1)
159+ }
160+
161+ function selectIndex(i) {
162+ // We could be anywhere in list; find target index to know which direction
163+ var greeter = findChild(shell, "greeter")
164+ var userlist = findChild(greeter, "userList")
165+ if (userlist.currentIndex == i)
166+ keyClick(Qt.Key_Escape) // Reset state if we're not moving
167+ while (userlist.currentIndex != i) {
168+ var next = userlist.currentIndex + 1
169+ if (userlist.currentIndex > i) {
170+ next = userlist.currentIndex - 1
171+ }
172+ var account = findChild(greeter, "username"+next)
173+ mouseClick(account, 1, 1)
174+ tryCompare(userlist, "currentIndex", next)
175+ tryCompare(userlist, "movingInternally", false)
176+ }
177+ }
178+
179+ function selectUser(name) {
180+ // Find index of user with the right name
181+ var greeter = findChild(shell, "greeter")
182+ for (var i = 0; i < greeter.model.count; i++) {
183+ if (greeter.model.data(i, LightDM.UserRoles.NameRole) == name) {
184+ break
185+ }
186+ }
187+ if (i == greeter.model.count) {
188+ fail("Didn't find name")
189+ return -1
190+ }
191+ selectIndex(i)
192+ return i
193+ }
194+
195+ function clickPasswordInput(isButton) {
196+ var greeter = findChild(shell, "greeter")
197+ tryCompare(greeter, "showProgress", 1)
198+
199+ var passwordMouseArea = findChild(shell, "passwordMouseArea")
200+ tryCompare(passwordMouseArea, "enabled", isButton)
201+
202+ var passwordInput = findChild(shell, "passwordInput")
203+ mouseClick(passwordInput, passwordInput.width / 2, passwordInput.height / 2)
204+ }
205+
206+ function confirmLoggedIn(loggedIn) {
207+ var greeterWrapper = findChild(shell, "greeterWrapper")
208+ tryCompare(greeterWrapper, "showProgress", loggedIn ? 0 : 1)
209+ tryCompare(sessionSpy, "count", loggedIn ? 1 : 0)
210+ }
211+
212+ function swipeFromLeftEdge(swipeLength) {
213+ var touchStartX = 2
214+ var touchStartY = shell.height / 2
215+ touchFlick(shell, touchStartX, touchStartY, swipeLength, touchStartY)
216+ }
217+
218+ function test_noLockscreen() {
219+ selectUser("has-password")
220+ var lockscreen = findChild(shell, "lockscreen")
221+ tryCompare(lockscreen, "shown", false)
222+ }
223+
224+ function test_showAndHideGreeterDBusCalls() {
225+ var greeter = findChild(shell, "greeter")
226+ LightDM.Greeter.hideGreeter()
227+ tryCompare(greeter, "showProgress", 0)
228+ LightDM.Greeter.showGreeter()
229+ tryCompare(greeter, "showProgress", 1)
230+ }
231+
232+ function test_login_data() {
233+ return [
234+ {tag: "auth error", user: "auth-error", loggedIn: false, password: ""},
235+ {tag: "with password", user: "has-password", loggedIn: true, password: "password"},
236+ {tag: "without password", user: "no-password", loggedIn: true, password: ""},
237+ ]
238+ }
239+
240+ function test_login(data) {
241+ selectUser(data.user)
242+
243+ clickPasswordInput(data.password === "")
244+
245+ if (data.password !== "") {
246+ typeString(data.password)
247+ keyClick(Qt.Key_Enter)
248+ }
249+
250+ confirmLoggedIn(data.loggedIn)
251+ }
252+
253+ function test_appLaunchDuringGreeter_data() {
254+ return [
255+ {tag: "auth error", user: "auth-error", loggedIn: false, passwordFocus: false},
256+ {tag: "without password", user: "no-password", loggedIn: true, passwordFocus: false},
257+ {tag: "with password", user: "has-password", loggedIn: false, passwordFocus: true},
258+ ]
259+ }
260+
261+ function test_appLaunchDuringGreeter(data) {
262+ selectUser(data.user)
263+
264+ var greeter = findChild(shell, "greeter")
265+ var app = ApplicationManager.startApplication("dialer-app")
266+
267+ confirmLoggedIn(data.loggedIn)
268+
269+ if (data.passwordFocus) {
270+ var passwordInput = findChild(greeter, "passwordInput")
271+ tryCompare(passwordInput, "focus", true)
272+ }
273+ }
274+
275+ function test_leftEdgeDrag_data() {
276+ return [
277+ {tag: "without password", user: "no-password", loggedIn: true},
278+ {tag: "with password", user: "has-password", loggedIn: false},
279+ ]
280+ }
281+
282+ function test_leftEdgeDrag(data) {
283+ selectUser(data.user)
284+ swipeFromLeftEdge(shell.width * 0.75)
285+ wait(500) // to give time to handle dash() signal from Launcher
286+ confirmLoggedIn(data.loggedIn)
287+ }
288+ }
289+}