on xenial help sign in
Merge lp:~dandrader/unity8/app-state-handling
commitab328db00cb4e600a3a805160ff8a3713f74c6e9 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateMon 3 Aug 2015, 11:32 (11 years ago) parent7c13987b8f72d1
22 files changed+468−121
CMakeLists.txt+1−1 debian/control+1−1 plugins/Greeter/Unity/Launcher/CMakeLists.txt−1 plugins/Unity/Launcher/CMakeLists.txt−1 qml/Components/Unity8Settings.qml+50 qml/OrientedShell.qml+3−3 qml/Shell.qml+10−12 qml/Stages/DesktopStage.qml+13 qml/Stages/PhoneStage.qml+10 qml/Stages/TabletStage.qml+14 tests/mocks/Unity/Application/ApplicationInfo.cpp+40−6 tests/mocks/Unity/Application/ApplicationInfo.h+7 tests/mocks/Unity/Application/ApplicationManager.cpp+2−49 tests/mocks/Unity/Application/ApplicationManager.h−7 tests/plugins/Greeter/Unity/Launcher/CMakeLists.txt−1 tests/plugins/Unity/Launcher/launchermodeltest.cpp+3−4 tests/qmltests/Stages/ApplicationCheckBox.qml+21 tests/qmltests/Stages/tst_DesktopStage.qml+2−2 tests/qmltests/Stages/tst_PhoneStage.qml+75−3 tests/qmltests/Stages/tst_TabletStage.qml+161−1 tests/qmltests/tst_OrientedShell.qml+3−3 tests/qmltests/tst_Shell.qml+52−26
CMakeLists.txt+1−1
@@ -56,7 +56,7 @@ find_package(Qt5DBus 5.2 REQUIRED)
5656find_package(Qt5Concurrent 5.2 REQUIRED)
5757find_package(Qt5Sql 5.2 REQUIRED)
5858
59−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
59+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=7)
6060
6161# Standard install paths
6262include(GNUInstallDirs)
debian/control+1−1
@@ -28,7 +28,7 @@ Build-Depends: cmake,
2828 libqt5xmlpatterns5-dev,
2929 libsystemsettings-dev,
3030 libudev-dev,
31− libunity-api-dev (>= 7.98),
31+ libunity-api-dev (>= 7.99),
3232 libusermetricsoutput1-dev,
3333 libxcb1-dev,
3434 pkg-config,
plugins/Greeter/Unity/Launcher/CMakeLists.txt−1
@@ -1,5 +1,4 @@
11pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
2−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
32pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
43
54add_definitions(-DSM_BUSNAME=systemBus)
plugins/Unity/Launcher/CMakeLists.txt−1
@@ -1,5 +1,4 @@
11pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
2−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
32pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
43
54add_definitions(-DSM_BUSNAME=systemBus)
qml/Components/Unity8Settings.qmlnew file+50
@@ -0,0 +1,50 @@
1+/*
2+ * Copyright (C) 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.0
18+import GSettings 1.0
19+
20+QtObject {
21+ id: root
22+
23+ property string usageMode: "Staged"
24+
25+ // FIXME: Works around a bug where if we change a Loader's source in response to a GSettings
26+ // property change in the same event loop, the Loader's previously loaded component does not
27+ // get destroyed and its bindings continue to operate!
28+ //
29+ // Shouldn't be needed after
30+ // https://code.launchpad.net/~lukas-kde/gsettings-qt/queued-processing/+merge/259883 gets
31+ // merged.
32+ property var timer: Timer {
33+ interval: 1
34+ onTriggered: { root.usageMode = root.wrapped.usageMode; }
35+ }
36+ property var wrappedConnections: Connections {
37+ target: root.wrapped
38+ ignoreUnknownSignals: true // don't spam us
39+ onUsageModeChanged: { root.timer.start(); }
40+ }
41+ property var wrapped: GSettings {
42+ schema.id: "com.canonical.Unity8"
43+ Component.onCompleted: {
44+ // init the value. it's a dynamic prop, so we have to check first
45+ if (root.usageMode) {
46+ root.usageMode = root.wrapped.usageMode;
47+ }
48+ }
49+ }
50+}
qml/OrientedShell.qml+3−3
@@ -43,7 +43,7 @@ Rectangle {
4343
4444
4545 // to be overwritten by tests
46− property var usageModeSettings: GSettings { schema.id: "com.canonical.Unity8" }
46+ property var unity8Settings: Unity8Settings {}
4747 property var oskSettings: GSettings { schema.id: "com.canonical.keyboard.maliit" }
4848
4949 property int physicalOrientation: Screen.orientation
@@ -160,9 +160,9 @@ Rectangle {
160160 // TODO: Factor in the connected input devices (eg: physical keyboard, mouse, touchscreen),
161161 // what's the output device (eg: big TV, desktop monitor, phone display), etc.
162162 usageScenario: {
163− if (root.usageModeSettings.usageMode === "Windowed") {
163+ if (root.unity8Settings.usageMode === "Windowed") {
164164 return "desktop";
165− } else if (root.usageModeSettings.usageMode === "Staged") {
165+ } else if (root.unity8Settings.usageMode === "Staged") {
166166 if (deviceConfiguration.category === "phone") {
167167 return "phone";
168168 } else {
qml/Shell.qml+10−12
@@ -191,12 +191,6 @@ Item {
191191 z: dialogs.z + 10
192192 }
193193
194− Binding {
195− target: ApplicationManager
196− property: "forceDashActive"
197− value: launcher.shown || launcher.dashSwipe
198− }
199−
200194 WindowKeysFilter {
201195 Keys.onPressed: physicalKeysMapper.onKeyPressed(event);
202196 Keys.onReleased: physicalKeysMapper.onKeyReleased(event);
@@ -352,6 +346,16 @@ Item {
352346 property: "beingResized"
353347 value: shell.beingResized
354348 }
349+ Binding {
350+ target: applicationsDisplayLoader.item
351+ property: "keepDashRunning"
352+ value: launcher.shown || launcher.dashSwipe
353+ }
354+ Binding {
355+ target: applicationsDisplayLoader.item
356+ property: "suspended"
357+ value: greeter.shown
358+ }
355359 Binding {
356360 target: applicationsDisplayLoader.item
357361 property: "altTabPressed"
@@ -459,12 +463,6 @@ Item {
459463 }
460464
461465 onEmergencyCall: startLockedApp("dialer-app")
462−
463− Binding {
464− target: ApplicationManager
465− property: "suspended"
466− value: greeter.shown
467− }
468466 }
469467 }
470468
qml/Stages/DesktopStage.qml+13
@@ -39,6 +39,8 @@ Rectangle {
3939 property int shellPrimaryOrientation
4040 property int nativeOrientation
4141 property bool beingResized: false
42+ property bool keepDashRunning: true
43+ property bool suspended: false
4244
4345 // functions to be called from outside
4446 function updateFocusedAppOrientation() { /* TODO */ }
@@ -215,6 +217,17 @@ Rectangle {
215217 }
216218 }
217219
220+ Binding {
221+ target: ApplicationManager.get(index)
222+ property: "requestedState"
223+ // TODO: figure out some lifecycle policy, like suspending minimized apps
224+ // if running on a tablet or something.
225+ // TODO: If the device has a dozen suspended apps because it was running
226+ // in staged mode, when it switches to Windowed mode it will suddenly
227+ // resume all those apps at once. We might want to avoid that.
228+ value: ApplicationInfoInterface.RequestedRunning // Always running for now
229+ }
230+
218231 function maximize() {
219232 minimized = false;
220233 maximized = true;
qml/Stages/PhoneStage.qml+10
@@ -36,6 +36,8 @@ Rectangle {
3636 property bool altTabEnabled: true
3737 property real startScale: 1.1
3838 property real endScale: 0.7
39+ property bool keepDashRunning: true
40+ property bool suspended: false
3941 property int shellOrientationAngle: 0
4042 property int shellOrientation
4143 property int shellPrimaryOrientation
@@ -428,6 +430,14 @@ Rectangle {
428430 dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
429431 focusFirstApp: root.focusFirstApp
430432
433+ Binding {
434+ target: appDelegate.application
435+ property: "requestedState"
436+ value: (isDash && root.keepDashRunning) || (!root.suspended && appDelegate.focus)
437+ ? ApplicationInfoInterface.RequestedRunning
438+ : ApplicationInfoInterface.RequestedSuspended
439+ }
440+
431441 readonly property bool isDash: model.appId == "unity8-dash"
432442
433443 z: isDash && !spreadView.active ? -1 : behavioredIndex
qml/Stages/TabletStage.qml+14
@@ -36,6 +36,8 @@ Rectangle {
3636 property bool spreadEnabled: true // If false, animations and right edge will be disabled
3737
3838 property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
39+ property bool keepDashRunning: true
40+ property bool suspended: false
3941 property int shellOrientationAngle: 0
4042 property int shellOrientation
4143 property int shellPrimaryOrientation
@@ -117,6 +119,7 @@ Rectangle {
117119
118120 QtObject {
119121 id: priv
122+ objectName: "stagesPriv"
120123
121124 property string focusedAppId: ApplicationManager.focusedApplicationId
122125 readonly property var focusedAppDelegate: {
@@ -626,6 +629,17 @@ Rectangle {
626629
627630 readonly property bool isDash: model.appId == "unity8-dash"
628631
632+ Binding {
633+ target: spreadTile.application
634+ property: "requestedState"
635+ value: (spreadTile.isDash && root.keepDashRunning)
636+ ||
637+ (!root.suspended && (model.appId == priv.mainStageAppId
638+ || model.appId == priv.sideStageAppId))
639+ ? ApplicationInfoInterface.RequestedRunning
640+ : ApplicationInfoInterface.RequestedSuspended
641+ }
642+
629643 // FIXME: A regular binding doesn't update any more after closing an app.
630644 // Using a Binding for now.
631645 Binding {
tests/mocks/Unity/Application/ApplicationInfo.cpp+40−6
@@ -30,7 +30,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
3030 : ApplicationInfoInterface(appId, parent)
3131 , m_appId(appId)
3232 , m_stage(MainStage)
33− , m_state(Starting)
33+ , m_state(Stopped)
3434 , m_focused(false)
3535 , m_fullscreen(false)
3636 , m_session(0)
@@ -39,6 +39,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
3939 Qt::InvertedPortraitOrientation |
4040 Qt::InvertedLandscapeOrientation)
4141 , m_rotatesWindowContents(false)
42+ , m_requestedState(RequestedRunning)
4243 , m_manualSurfaceCreation(false)
4344{
4445}
@@ -46,7 +47,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
4647ApplicationInfo::ApplicationInfo(QObject *parent)
4748 : ApplicationInfoInterface(QString(), parent)
4849 , m_stage(MainStage)
49− , m_state(Starting)
50+ , m_state(Stopped)
5051 , m_focused(false)
5152 , m_fullscreen(false)
5253 , m_session(0)
@@ -55,6 +56,7 @@ ApplicationInfo::ApplicationInfo(QObject *parent)
5556 Qt::InvertedPortraitOrientation |
5657 Qt::InvertedLandscapeOrientation)
5758 , m_rotatesWindowContents(false)
59+ , m_requestedState(RequestedRunning)
5860 , m_manualSurfaceCreation(false)
5961{
6062}
@@ -74,7 +76,6 @@ void ApplicationInfo::createSession()
7476
7577void ApplicationInfo::setSession(Session* session)
7678{
77− qDebug() << "Application::setSession - appId=" << appId() << "session=" << session;
7879 if (m_session == session)
7980 return;
8081
@@ -91,6 +92,8 @@ void ApplicationInfo::setSession(Session* session)
9192 m_session->setApplication(this);
9293 m_session->setParent(this);
9394 SessionManager::singleton()->registerSession(m_session);
95+ connect(m_session, &Session::surfaceChanged,
96+ this, &ApplicationInfo::onSessionSurfaceChanged);
9497
9598 if (!m_manualSurfaceCreation) {
9699 QTimer::singleShot(500, m_session, SLOT(createSurface()));
@@ -161,11 +164,12 @@ void ApplicationInfo::setState(State value)
161164 m_state = value;
162165 Q_EMIT stateChanged(value);
163166
164− if (!m_manualSurfaceCreation && m_state == ApplicationInfo::Running) {
167+ if (!m_manualSurfaceCreation && !m_session && m_state == ApplicationInfo::Starting) {
165168 QTimer::singleShot(500, this, SLOT(createSession()));
166169 } else if (m_state == ApplicationInfo::Stopped) {
167− delete m_session;
168− m_session = nullptr;
170+ Session *session = m_session;
171+ setSession(nullptr);
172+ delete session;
169173 }
170174 }
171175}
@@ -213,3 +217,33 @@ void ApplicationInfo::setRotatesWindowContents(bool value)
213217{
214218 m_rotatesWindowContents = value;
215219}
220+
221+ApplicationInfo::RequestedState ApplicationInfo::requestedState() const
222+{
223+ return m_requestedState;
224+}
225+
226+void ApplicationInfo::setRequestedState(RequestedState value)
227+{
228+ if (m_requestedState != value) {
229+ m_requestedState = value;
230+ Q_EMIT requestedStateChanged(m_requestedState);
231+
232+ if (m_requestedState == RequestedRunning && m_state == Suspended) {
233+ setState(Running);
234+ } else if (m_requestedState == RequestedSuspended && m_state == Running) {
235+ setState(Suspended);
236+ }
237+ }
238+}
239+
240+void ApplicationInfo::onSessionSurfaceChanged(MirSurfaceItem* surface)
241+{
242+ if (surface != nullptr && m_state == Starting) {
243+ if (m_requestedState == RequestedRunning) {
244+ setState(Running);
245+ } else {
246+ setState(Suspended);
247+ }
248+ }
249+}
tests/mocks/Unity/Application/ApplicationInfo.h+7
@@ -46,6 +46,9 @@ public:
4646 ApplicationInfo(const QString &appId, QObject *parent = nullptr);
4747 ~ApplicationInfo();
4848
49+ RequestedState requestedState() const override;
50+ void setRequestedState(RequestedState) override;
51+
4952 void setIconId(const QString &iconId);
5053 void setScreenshotId(const QString &screenshotId);
5154
@@ -101,6 +104,9 @@ Q_SIGNALS:
101104public Q_SLOTS:
102105 Q_INVOKABLE void createSession();
103106
107+private Q_SLOTS:
108+ void onSessionSurfaceChanged(MirSurfaceItem*);
109+
104110private:
105111 void setIcon(const QUrl &value);
106112
@@ -116,6 +122,7 @@ private:
116122 Session* m_session;
117123 Qt::ScreenOrientations m_supportedOrientations;
118124 bool m_rotatesWindowContents;
125+ RequestedState m_requestedState;
119126
120127 bool m_manualSurfaceCreation;
121128};
tests/mocks/Unity/Application/ApplicationManager.cpp+2−49
@@ -49,8 +49,6 @@ ApplicationManager *ApplicationManager::singleton()
4949
5050ApplicationManager::ApplicationManager(QObject *parent)
5151 : ApplicationManagerInterface(parent)
52− , m_suspended(false)
53− , m_forceDashActive(false)
5452{
5553 m_roleNames.insert(RoleSession, "session");
5654 m_roleNames.insert(RoleFullscreen, "fullscreen");
@@ -217,7 +215,7 @@ ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
217215 && application->stage() == ApplicationInfo::SideStage) {
218216 application->setStage(ApplicationInfo::MainStage);
219217 }
220− application->setState(ApplicationInfo::Running);
218+ application->setState(ApplicationInfo::Starting);
221219
222220 return application;
223221}
@@ -262,50 +260,6 @@ QString ApplicationManager::focusedApplicationId() const {
262260 return QString();
263261}
264262
265−bool ApplicationManager::suspended() const
266−{
267− return m_suspended;
268−}
269−
270−void ApplicationManager::setSuspended(bool suspended)
271−{
272− ApplicationInfo *focusedApp = findApplication(focusedApplicationId());
273− if (focusedApp) {
274− if (suspended) {
275− focusedApp->setState(ApplicationInfo::Suspended);
276− } else {
277− focusedApp->setState(ApplicationInfo::Running);
278− }
279− }
280− m_suspended = suspended;
281− Q_EMIT suspendedChanged();
282−}
283−
284−bool ApplicationManager::forceDashActive() const
285−{
286− return m_forceDashActive;
287−}
288−
289−void ApplicationManager::setForceDashActive(bool forceDashActive)
290−{
291− if (m_forceDashActive == forceDashActive) {
292− return;
293− }
294−
295− ApplicationInfo *dash = findApplication("unity8-dash");
296− if (dash) {
297− if (forceDashActive) {
298− dash->setState(ApplicationInfo::Running);
299− } else {
300− if (!dash->focused()) {
301− dash->setState(ApplicationInfo::Suspended);
302− }
303− }
304− }
305− m_forceDashActive = forceDashActive;
306− Q_EMIT forceDashActiveChanged();
307−}
308−
309263bool ApplicationManager::focusApplication(const QString &appId)
310264{
311265 ApplicationInfo *application = findApplication(appId);
@@ -316,13 +270,11 @@ bool ApplicationManager::focusApplication(const QString &appId)
316270 for (ApplicationInfo *app : m_runningApplications) {
317271 if (app->focused()) {
318272 app->setFocused(false);
319− app->setState(ApplicationInfo::Suspended);
320273 }
321274 }
322275
323276 // focus this app
324277 application->setFocused(true);
325− application->setState(ApplicationInfo::Running);
326278
327279 // move app to top of stack
328280 move(m_runningApplications.indexOf(application), 0);
@@ -394,6 +346,7 @@ void ApplicationManager::buildListOfAvailableApplications()
394346 application->setScreenshotId("gallery");
395347 application->setIconId("gallery");
396348 application->setFullscreen(true);
349+ application->setStage(ApplicationInfo::MainStage);
397350 m_availableApplications.append(application);
398351
399352 application = new ApplicationInfo(this);
tests/mocks/Unity/Application/ApplicationManager.h−7
@@ -69,11 +69,6 @@ class ApplicationManager : public ApplicationManagerInterface {
6969 Q_INVOKABLE bool stopApplication(const QString &appId) override;
7070
7171 QString focusedApplicationId() const override;
72− bool suspended() const override;
73− void setSuspended(bool suspended) override;
74−
75− bool forceDashActive() const override;
76− void setForceDashActive(bool forceDashActive) override;
7772
7873 // Only for testing
7974 QStringList availableApplications();
@@ -96,8 +91,6 @@ class ApplicationManager : public ApplicationManagerInterface {
9691 void remove(ApplicationInfo* application);
9792 void buildListOfAvailableApplications();
9893 void onWindowCreated();
99− bool m_suspended;
100− bool m_forceDashActive;
10194 QList<ApplicationInfo*> m_runningApplications;
10295 QList<ApplicationInfo*> m_availableApplications;
10396 QTimer m_windowCreatedTimer;
tests/plugins/Greeter/Unity/Launcher/CMakeLists.txt−1
@@ -1,5 +1,4 @@
11pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
2−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
32
43include_directories(
54 ${CMAKE_CURRENT_SOURCE_DIR}
tests/plugins/Unity/Launcher/launchermodeltest.cpp+3−4
@@ -39,6 +39,9 @@ class MockApp: public unity::shell::application::ApplicationInfoInterface
3939 Q_OBJECT
4040public:
4141 MockApp(const QString &appId, QObject *parent = 0): ApplicationInfoInterface(appId, parent), m_appId(appId), m_focused(false) { }
42+
43+ RequestedState requestedState() const override { return RequestedRunning; }
44+ void setRequestedState(RequestedState) override {}
4245 QString appId() const override { return m_appId; }
4346 QString name() const override { return "mock"; }
4447 QString comment() const override { return "this is a mock"; }
@@ -117,10 +120,6 @@ public:
117120 endRemoveRows();
118121 }
119122 bool requestFocusApplication(const QString &appId) override { Q_UNUSED(appId); return true; }
120− bool suspended() const override { return false; }
121− void setSuspended(bool) override {}
122− bool forceDashActive() const override { return false; }
123− void setForceDashActive(bool) override {}
124123
125124private:
126125 QList<MockApp*> m_list;
tests/qmltests/Stages/ApplicationCheckBox.qml+21
@@ -86,8 +86,29 @@ RowLayout {
8686 }
8787 }
8888 Label {
89+ id: appIdLabel
8990 text: root.appId
9091 color: "white"
9192 anchors.verticalCenter: parent.verticalCenter
9293 }
94+ Rectangle {
95+ color: {
96+ if (d.application) {
97+ if (d.application.state === ApplicationInfoInterface.Starting) {
98+ return "yellow";
99+ } else if (d.application.state === ApplicationInfoInterface.Running) {
100+ return "green";
101+ } else if (d.application.state === ApplicationInfoInterface.Suspended) {
102+ return "blue";
103+ } else {
104+ return "darkred";
105+ }
106+ } else {
107+ return "darkred";
108+ }
109+ }
110+ width: height
111+ height: appIdLabel.height * 0.7
112+ anchors.verticalCenter: parent.verticalCenter
113+ }
93114}
tests/qmltests/Stages/tst_DesktopStage.qml+2−2
@@ -24,9 +24,8 @@ import Utils 0.1
2424
2525import "../../../qml/Stages"
2626
27−Rectangle {
27+Item {
2828 id: root
29− color: "darkblue"
3029 width: desktopStageLoader.width + controls.width
3130 height: desktopStageLoader.height
3231
@@ -57,6 +56,7 @@ Rectangle {
5756 property bool itemDestroyed: false
5857 sourceComponent: Component {
5958 DesktopStage {
59+ color: "darkblue"
6060 anchors.fill: parent
6161 Component.onDestruction: {
6262 desktopStageLoader.itemDestroyed = true;
tests/qmltests/Stages/tst_PhoneStage.qml+75−3
@@ -301,16 +301,16 @@ Item {
301301 addApps(2);
302302
303303 var secondApp = ApplicationManager.get(0);
304− tryCompare(secondApp, "state", ApplicationInfoInterface.Running);
304+ tryCompare(secondApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
305305 tryCompare(secondApp, "focused", true);
306306
307307 var firstApp = ApplicationManager.get(1);
308− tryCompare(firstApp, "state", ApplicationInfoInterface.Suspended);
308+ tryCompare(firstApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
309309 tryCompare(firstApp, "focused", false);
310310
311311 ApplicationManager.stopApplication(secondApp.appId);
312312
313− tryCompare(firstApp, "state", ApplicationInfoInterface.Running);
313+ tryCompare(firstApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
314314 tryCompare(firstApp, "focused", true);
315315 }
316316
@@ -388,5 +388,77 @@ Item {
388388
389389 tryCompare(focusedDelegate, "x", 0);
390390 }
391+
392+ function test_focusedAppIsTheOnlyRunningApp() {
393+ addApps(2);
394+
395+ var delegateA = findChild(phoneStage, "appDelegate0");
396+ verify(delegateA);
397+ var delegateB = findChild(phoneStage, "appDelegate1");
398+ verify(delegateB);
399+
400+ // A is focused and running, B is unfocused and suspended
401+ compare(delegateA.focus, true);
402+ compare(delegateA.application.requestedState, ApplicationInfoInterface.RequestedRunning);
403+ compare(delegateB.focus, false);
404+ compare(delegateB.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
405+
406+ // Switch foreground/focused appp from A to B
407+ goToSpread();
408+ phoneStage.select(delegateB.application.appId);
409+
410+ // Now it's the other way round
411+ // A is unfocused and suspended, B is focused and running
412+ tryCompare(delegateA, "focus", false);
413+ tryCompare(delegateA.application, "requestedState", ApplicationInfoInterface.RequestedSuspended);
414+ tryCompare(delegateB, "focus", true);
415+ tryCompare(delegateB.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
416+ }
417+
418+ function test_dashRemainsRunningIfStageIsToldSo() {
419+ addApps(1);
420+
421+ var delegateDash = findChild(phoneStage, "appDelegate1");
422+ verify(delegateDash);
423+ compare(delegateDash.application.appId, "unity8-dash");
424+
425+ var delegateOther = findChild(phoneStage, "appDelegate0");
426+ verify(delegateOther);
427+
428+ goToSpread();
429+ phoneStage.select("unity8-dash");
430+
431+ tryCompare(delegateDash, "focus", true);
432+ tryCompare(delegateDash.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
433+ compare(delegateOther.focus, false);
434+ compare(delegateOther.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
435+
436+ goToSpread();
437+ phoneStage.select(delegateOther.application.appId);
438+
439+ // The other app gets focused and running but dash is kept running despite being unfocused
440+ tryCompare(delegateDash, "focus", false);
441+ tryCompare(delegateDash.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
442+ compare(delegateOther.focus, true);
443+ compare(delegateOther.application.requestedState, ApplicationInfoInterface.RequestedRunning);
444+ }
445+
446+ function test_foregroundAppIsSuspendedWhenStageIsSuspended() {
447+ addApps(1);
448+
449+ var delegate = findChild(phoneStage, "appDelegate0");
450+ verify(delegate);
451+
452+ compare(delegate.focus, true);
453+ compare(delegate.application.requestedState, ApplicationInfoInterface.RequestedRunning);
454+
455+ phoneStage.suspended = true;
456+
457+ tryCompare(delegate.application, "requestedState", ApplicationInfoInterface.RequestedSuspended);
458+
459+ phoneStage.suspended = false;
460+
461+ tryCompare(delegate.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
462+ }
391463 }
392464}
tests/qmltests/Stages/tst_TabletStage.qml+161−1
@@ -76,10 +76,18 @@ Rectangle {
7676 id: webbrowserCheckBox
7777 appId: "webbrowser-app"
7878 }
79+ ApplicationCheckBox {
80+ id: galleryCheckBox
81+ appId: "gallery-app"
82+ }
7983 ApplicationCheckBox {
8084 id: dialerCheckBox
8185 appId: "dialer-app"
8286 }
87+ ApplicationCheckBox {
88+ id: facebookCheckBox
89+ appId: "facebook-webapp"
90+ }
8391 }
8492 }
8593
@@ -93,6 +101,19 @@ Rectangle {
93101 function init() {
94102 tabletStageLoader.active = true;
95103 tryCompare(tabletStageLoader, "status", Loader.Ready);
104+
105+ // this is very strange, but sometimes the test starts without
106+ // TabletStage components having finished loading themselves
107+ var appWindow = null;
108+ while (!appWindow) {
109+ appWindow = findChild(tabletStage, "appWindow_unity8-dash");
110+ if (!appWindow) {
111+ console.log("didn't find appWindow_unity8-dash in " + tabletStage + ". Trying again...");
112+ wait(50);
113+ }
114+ }
115+
116+ waitUntilAppSurfaceShowsUp("unity8-dash");
96117 }
97118
98119 function cleanup() {
@@ -109,7 +130,9 @@ Rectangle {
109130
110131 // kill all (fake) running apps
111132 webbrowserCheckBox.checked = false;
133+ galleryCheckBox.checked = false;
112134 dialerCheckBox.checked = false;
135+ facebookCheckBox.checked = false;
113136 }
114137
115138 function waitUntilAppSurfaceShowsUp(appId) {
@@ -120,6 +143,34 @@ Rectangle {
120143 tryCompare(appWindowStates, "state", "surface");
121144 }
122145
146+ function switchToApp(targetAppId) {
147+ touchFlick(tabletStage,
148+ tabletStage.width - (tabletStage.dragAreaWidth / 2), tabletStage.height / 2,
149+ tabletStage.x + 1, tabletStage.height / 2);
150+
151+ var spreadView = findChild(tabletStage, "spreadView");
152+ verify(spreadView);
153+ tryCompare(spreadView, "phase", 2);
154+ tryCompare(spreadView, "flicking", false);
155+ tryCompare(spreadView, "moving", false);
156+
157+ waitUntilAppDelegateStopsMoving(targetAppId);
158+
159+ var targetAppWindow = findChild(tabletStage, "appWindow_" + targetAppId);
160+ tap(targetAppWindow, 10, 10);
161+ }
162+
163+ function waitUntilAppDelegateStopsMoving(targetAppId)
164+ {
165+ var targetAppDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + targetAppId);
166+ verify(targetAppDelegate);
167+ var lastValue = undefined;
168+ do {
169+ lastValue = targetAppDelegate.animatedProgress;
170+ wait(30);
171+ } while (lastValue != targetAppDelegate.animatedProgress);
172+ }
173+
123174 function test_tappingSwitchesFocusBetweenStages() {
124175 webbrowserCheckBox.checked = true;
125176 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
@@ -155,7 +206,6 @@ Rectangle {
155206 tryCompare(webbrowserApp.session.surface, "activeFocus", false);
156207 }
157208
158−
159209 function test_closeAppInSideStage() {
160210 dialerCheckBox.checked = true;
161211 waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
@@ -200,6 +250,116 @@ Rectangle {
200250 appWindow.width / 2, appWindow.height / 2,
201251 appWindow.width / 2, -appWindow.height / 2);
202252 }
253+
254+ function test_suspendsAndResumesAppsInMainStage() {
255+ webbrowserCheckBox.checked = true;
256+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
257+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
258+
259+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
260+
261+ galleryCheckBox.checked = true;
262+ var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
263+ compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
264+
265+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
266+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
267+
268+ switchToApp(webbrowserApp.appId);
269+
270+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
271+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
272+
273+ switchToApp(galleryApp.appId);
274+
275+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
276+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
277+ }
278+
279+
280+ function test_foregroundMainAndSideStageAppsAreKeptRunning() {
281+
282+ var stagesPriv = findInvisibleChild(tabletStage, "stagesPriv");
283+ verify(stagesPriv);
284+
285+ // launch two main stage apps
286+ // gallery will be on foreground and webbrowser on background
287+
288+ webbrowserCheckBox.checked = true;
289+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
290+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
291+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
292+
293+ galleryCheckBox.checked = true;
294+ waitUntilAppSurfaceShowsUp(galleryCheckBox.appId)
295+ var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
296+ compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
297+
298+ compare(stagesPriv.mainStageAppId, galleryCheckBox.appId);
299+
300+ // then launch two side stage apps
301+ // facebook will be on foreground and dialer on background
302+
303+ dialerCheckBox.checked = true;
304+ waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
305+ var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
306+ compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
307+
308+ facebookCheckBox.checked = true;
309+ waitUntilAppSurfaceShowsUp(facebookCheckBox.appId)
310+ var facebookApp = ApplicationManager.findApplication(facebookCheckBox.appId);
311+ compare(facebookApp.stage, ApplicationInfoInterface.SideStage);
312+
313+ compare(stagesPriv.sideStageAppId, facebookCheckBox.appId);
314+
315+ // Now check that the foreground apps are running and that the background ones
316+ // are suspended
317+
318+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
319+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
320+ tryCompare(facebookApp, "state", ApplicationInfoInterface.Running);
321+ tryCompare(dialerApp, "state", ApplicationInfoInterface.Suspended);
322+
323+ switchToApp(dialerCheckBox.appId);
324+
325+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
326+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
327+ tryCompare(facebookApp, "state", ApplicationInfoInterface.Suspended);
328+ tryCompare(dialerApp, "state", ApplicationInfoInterface.Running);
329+
330+ switchToApp(webbrowserCheckBox.appId);
331+
332+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
333+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
334+ tryCompare(facebookApp, "state", ApplicationInfoInterface.Suspended);
335+ tryCompare(dialerApp, "state", ApplicationInfoInterface.Running);
336+ }
337+
338+ function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
339+ webbrowserCheckBox.checked = true;
340+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
341+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
342+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
343+
344+ dialerCheckBox.checked = true;
345+ waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
346+ var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
347+ compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
348+
349+
350+ compare(webbrowserApp.requestedState, ApplicationInfoInterface.RequestedRunning);
351+ compare(dialerApp.requestedState, ApplicationInfoInterface.RequestedRunning);
352+
353+ tabletStage.suspended = true;
354+
355+ tryCompare(webbrowserApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
356+ tryCompare(dialerApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
357+
358+ tabletStage.suspended = false;
359+
360+ tryCompare(webbrowserApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
361+ tryCompare(dialerApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
362+ }
203363 }
204364
205365}
tests/qmltests/tst_OrientedShell.qml+3−3
@@ -46,7 +46,7 @@ Rectangle {
4646 }
4747
4848 QtObject {
49− id: mockUsageModeSettings
49+ id: mockUnity8Settings
5050 property string usageMode: usageModeSelector.model[usageModeSelector.selectedIndex]
5151 }
5252
@@ -125,7 +125,7 @@ Rectangle {
125125 sourceComponent: Component {
126126 OrientedShell {
127127 anchors.fill: parent
128− usageModeSettings: mockUsageModeSettings
128+ unity8Settings: mockUnity8Settings
129129 oskSettings: mockOskSettings
130130 physicalOrientation: root.physicalOrientation0
131131 orientationLocked: orientationLockedCheckBox.checked
@@ -926,7 +926,7 @@ Rectangle {
926926
927927 function test_attachRemoveInputDevices() {
928928 usageModeSelector.selectedIndex = 2;
929− tryCompare(mockUsageModeSettings, "usageMode", "Automatic")
929+ tryCompare(mockUnity8Settings, "usageMode", "Automatic")
930930
931931 loadShell("mako")
932932 var shell = findChild(orientedShell, "shell");
tests/qmltests/tst_Shell.qml+52−26
@@ -67,11 +67,6 @@ Rectangle {
6767 width: units.gu(40)
6868 height: units.gu(71)
6969 }
70− StateChangeScript {
71− script: {
72− GSettingsController.setUsageMode("Staged")
73− }
74− }
7570 },
7671 State {
7772 name: "tablet"
@@ -80,11 +75,6 @@ Rectangle {
8075 width: units.gu(100)
8176 height: units.gu(71)
8277 }
83− StateChangeScript {
84− script: {
85− GSettingsController.setUsageMode("Staged")
86− }
87− }
8878 },
8979 State {
9080 name: "desktop"
@@ -97,11 +87,6 @@ Rectangle {
9787 target: mouseEmulation
9888 checked: false
9989 }
100− StateChangeScript {
101− script: {
102− GSettingsController.setUsageMode("Windowed")
103− }
104− }
10590 }
10691 ]
10792
@@ -317,7 +302,6 @@ Rectangle {
317302 mouseEmulation.checked = true;
318303 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
319304 tearDown();
320− GSettingsController.setUsageMode("Staged");
321305 }
322306
323307 function loadShell(formFactor) {
@@ -602,9 +586,9 @@ Rectangle {
602586 verify(mainAppId != "");
603587 var mainApp = ApplicationManager.findApplication(mainAppId);
604588 verify(mainApp);
605− tryCompare(mainApp, "state", ApplicationInfoInterface.Running);
589+ tryCompare(mainApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
606590
607− // Suspend while call is active...
591+ // Display off while call is active...
608592 callManager.foregroundCall = phoneCall;
609593 Powerd.status = Powerd.Off;
610594 tryCompare(greeter, "shown", false);
@@ -615,8 +599,7 @@ Rectangle {
615599 Powerd.status = Powerd.Off;
616600 tryCompare(greeter, "fullyShown", true);
617601
618− tryCompare(ApplicationManager, "suspended", true);
619− compare(mainApp.state, ApplicationInfoInterface.Suspended);
602+ compare(mainApp.requestedState, ApplicationInfoInterface.RequestedSuspended);
620603
621604 // And wake up
622605 Powerd.status = Powerd.On;
@@ -632,8 +615,7 @@ Rectangle {
632615 removeTimeConstraintsFromDirectionalDragAreas(greeter);
633616 swipeAwayGreeter();
634617
635− tryCompare(ApplicationManager, "suspended", false);
636− compare(mainApp.state, ApplicationInfoInterface.Running);
618+ compare(mainApp.requestedState, ApplicationInfoInterface.RequestedRunning);
637619 tryCompare(ApplicationManager, "focusedApplicationId", mainAppId);
638620 }
639621
@@ -804,15 +786,15 @@ Rectangle {
804786
805787 function test_launchedAppHasActiveFocus_data() {
806788 return [
807− {tag: "phone", formFactor: "phone", usageMode: "Staged"},
808− {tag: "tablet", formFactor: "tablet", usageMode: "Staged"},
809− {tag: "desktop", formFactor: "tablet", usageMode: "Windowed"}
789+ {tag: "phone", formFactor: "phone", usageScenario: "phone"},
790+ {tag: "tablet", formFactor: "tablet", usageScenario: "tablet"},
791+ {tag: "desktop", formFactor: "tablet", usageScenario: "desktop"}
810792 ]
811793 }
812794
813795 function test_launchedAppHasActiveFocus(data) {
814− GSettingsController.setUsageMode(data.usageMode);
815796 loadShell(data.formFactor);
797+ shell.usageScenario = data.usageScenario;
816798 swipeAwayGreeter();
817799
818800 var webApp = ApplicationManager.startApplication("webbrowser-app");
@@ -1347,6 +1329,50 @@ Rectangle {
13471329 compare(launcher.inverted, data.launcherInverted);
13481330 }
13491331
1332+ function test_unfocusedAppsGetSuspendedAfterEnteringStagedMode() {
1333+ loadShell("tablet");
1334+ shell.usageScenario = "desktop";
1335+
1336+ var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1337+ waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1338+
1339+ var galleryApp = ApplicationManager.startApplication("gallery-app");
1340+ waitUntilAppWindowIsFullyLoaded(galleryApp);
1341+
1342+ ApplicationManager.requestFocusApplication("unity8-dash");
1343+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1344+
1345+ compare(webBrowserApp.requestedState, ApplicationInfoInterface.RequestedRunning);
1346+ compare(galleryApp.requestedState, ApplicationInfoInterface.RequestedRunning);
1347+
1348+ shell.usageScenario = "tablet";
1349+
1350+ tryCompare(webBrowserApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
1351+ tryCompare(galleryApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
1352+ }
1353+
1354+ function test_unfocusedAppsAreResumedWhenEnteringWindowedMode() {
1355+ loadShell("tablet");
1356+ shell.usageScenario = "tablet";
1357+
1358+ var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1359+ waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1360+
1361+ var galleryApp = ApplicationManager.startApplication("gallery-app");
1362+ waitUntilAppWindowIsFullyLoaded(galleryApp);
1363+
1364+ ApplicationManager.requestFocusApplication("unity8-dash");
1365+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1366+
1367+ compare(webBrowserApp.requestedState, ApplicationInfoInterface.RequestedSuspended);
1368+ compare(galleryApp.requestedState, ApplicationInfoInterface.RequestedSuspended);
1369+
1370+ shell.usageScenario = "desktop";
1371+
1372+ tryCompare(webBrowserApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
1373+ tryCompare(galleryApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
1374+ }
1375+
13501376 function test_altTabSwitchesFocus() {
13511377 loadShell("desktop");
13521378 shell.usageScenario = "desktop"