Merge lp:~ken-vandine/ubuntu-system-settings/apn_refactor into lp:ubuntu-system-settings

Proposed by Ken VanDine
Status: Merged
Approved by: Ken VanDine
Approved revision: 1482
Merged at revision: 1488
Proposed branch: lp:~ken-vandine/ubuntu-system-settings/apn_refactor
Merge into: lp:ubuntu-system-settings
Diff against target: 3487 lines (+2067/-1140)
16 files modified
plugins/cellular/CMakeLists.txt (+3/-4)
plugins/cellular/Components/CMakeLists.txt (+3/-0)
plugins/cellular/Components/KeyboardRectangle.qml (+74/-0)
plugins/cellular/Components/LabelTextField.qml (+54/-0)
plugins/cellular/Components/StandardAnimation.qml (+22/-0)
plugins/cellular/PageApnEditor.qml (+394/-346)
plugins/cellular/PageChooseApn.qml (+333/-535)
plugins/cellular/apn_editor.js (+192/-0)
plugins/cellular/apn_manager.js (+588/-60)
plugins/cellular/ofonoactivator.cpp (+0/-132)
plugins/cellular/ofonoactivator.h (+0/-42)
plugins/cellular/plugin.cpp (+0/-2)
tests/autopilot/ubuntu_system_settings/__init__.py (+147/-1)
tests/autopilot/ubuntu_system_settings/tests/__init__.py (+17/-18)
tests/autopilot/ubuntu_system_settings/tests/ofono.py (+99/-0)
tests/autopilot/ubuntu_system_settings/tests/test_cellular.py (+141/-0)
To merge this branch: bzr merge lp:~ken-vandine/ubuntu-system-settings/apn_refactor
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Needs Fixing
Ken VanDine Approve
Review via email: mp+265973@code.launchpad.net

Commit message

[cellular/apn] new APN panel made for the new 'Preferred' property, as well as the 'ResetContext' method.

Description of the change

New UI due to new NetworkManager, Nuntium and Ofono backend.

Silo: 33

* Is your branch in sync with latest trunk (e.g. bzr pull lp:trunk -> no changes)
Yes
 * Did you build your software in a clean sbuild/pbuilder chroot or ppa?
Yes
 * Did you build your software in a clean sbuild/pbuilder armhf chroot or ppa?
Yes
 * Has your component "TestPlan” been executed successfully on emulator, N4?
No, it's currently being written AND the NetworkManager backend change has not landed yet.
 * Has a 5 minute exploratory testing run been executed on N4?
N/A
 * If you changed the packaging (debian), did you subscribe a core-dev to this MP?
Yes, kenvandine
 * If you changed the UI, did you subscribe the design-reviewers to this MP?
mpt
 * What components might get impacted by your changes?
NetworkManager, Nuntium, Ofono
 * Have you requested review by the teams of these owning components?
Yes

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Ken VanDine (ken-vandine) wrote :

This is just fixing a merge conflict from jonas' branch, which is approved.

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'plugins/cellular/CMakeLists.txt'
--- plugins/cellular/CMakeLists.txt 2015-03-26 21:57:53 +0000
+++ plugins/cellular/CMakeLists.txt 2015-07-27 14:21:15 +0000
@@ -4,9 +4,10 @@
4install(FILES settings-cellular.svg DESTINATION ${PLUGIN_MANIFEST_DIR}/icons)4install(FILES settings-cellular.svg DESTINATION ${PLUGIN_MANIFEST_DIR}/icons)
55
6set(QML_SOURCES6set(QML_SOURCES
7 apn.js7 apn_manager.js
8 apn_editor.js
8 carriers.js9 carriers.js
9 CustomApnEditor.qml10 PageApnEditor.qml
10 PageChooseApn.qml11 PageChooseApn.qml
11 PageChooseCarrier.qml12 PageChooseCarrier.qml
12 PageCarrierAndApn.qml13 PageCarrierAndApn.qml
@@ -23,8 +24,6 @@
23 plugin.h24 plugin.h
24 hotspotmanager.cpp25 hotspotmanager.cpp
25 hotspotmanager.h26 hotspotmanager.h
26 ofonoactivator.cpp
27 ofonoactivator.h
28 connectivity.cpp27 connectivity.cpp
29 connectivity.h28 connectivity.h
30 nm_manager_proxy.h29 nm_manager_proxy.h
3130
=== modified file 'plugins/cellular/Components/CMakeLists.txt'
--- plugins/cellular/Components/CMakeLists.txt 2014-11-12 12:46:51 +0000
+++ plugins/cellular/Components/CMakeLists.txt 2015-07-27 14:21:15 +0000
@@ -1,12 +1,15 @@
1set(QML_SOURCES1set(QML_SOURCES
2 DataMultiSim.qml2 DataMultiSim.qml
3 DefaultSim.qml3 DefaultSim.qml
4 KeyboardRectangle.qml
5 LabelTextField.qml
4 MultiSim.qml6 MultiSim.qml
5 NoSim.qml7 NoSim.qml
6 RadioSingleSim.qml8 RadioSingleSim.qml
7 Sim.qml9 Sim.qml
8 SimEditor.qml10 SimEditor.qml
9 SingleSim.qml11 SingleSim.qml
12 StandardAnimation.qml
10)13)
11install(FILES ${QML_SOURCES} DESTINATION ${PLUGIN_QML_DIR}/cellular/Components)14install(FILES ${QML_SOURCES} DESTINATION ${PLUGIN_QML_DIR}/cellular/Components)
1215
1316
=== added file 'plugins/cellular/Components/KeyboardRectangle.qml'
--- plugins/cellular/Components/KeyboardRectangle.qml 1970-01-01 00:00:00 +0000
+++ plugins/cellular/Components/KeyboardRectangle.qml 2015-07-27 14:21:15 +0000
@@ -0,0 +1,74 @@
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
17import QtQuick 2.2
18
19Item {
20 id: keyboardRect
21 anchors.left: parent.left
22 anchors.right: parent.right
23 anchors.bottom: parent.bottom
24 height: Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0
25
26 Behavior on height {
27 StandardAnimation { }
28 }
29
30 states: [
31 State {
32 name: "hidden"
33 when: keyboardRect.height == 0
34 },
35 State {
36 name: "shown"
37 when: keyboardRect.height == Qt.inputMethod.keyboardRectangle.height
38 }
39 ]
40
41 function recursiveFindFocusedItem(parent) {
42 if (parent.activeFocus) {
43 return parent;
44 }
45
46 for (var i in parent.children) {
47 var child = parent.children[i];
48 if (child.activeFocus) {
49 return child;
50 }
51
52 var item = recursiveFindFocusedItem(child);
53
54 if (item != null) {
55 return item;
56 }
57 }
58
59 return null;
60 }
61
62 Connections {
63 target: Qt.inputMethod
64
65 onVisibleChanged: {
66 if (!Qt.inputMethod.visible) {
67 var focusedItem = recursiveFindFocusedItem(keyboardRect.parent);
68 if (focusedItem != null) {
69 focusedItem.focus = false;
70 }
71 }
72 }
73 }
74}
075
=== added file 'plugins/cellular/Components/LabelTextField.qml'
--- plugins/cellular/Components/LabelTextField.qml 1970-01-01 00:00:00 +0000
+++ plugins/cellular/Components/LabelTextField.qml 2015-07-27 14:21:15 +0000
@@ -0,0 +1,54 @@
1/*
2 * This file is part of system-settings
3 *
4 * Copyright (C) 2015 Canonical Ltd.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License version 3, as published
8 * by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranties of
12 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
13 * PURPOSE. See the GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.0
20import Ubuntu.Components 1.1
21import Ubuntu.Components.Themes.Ambiance 0.1
22import Ubuntu.Keyboard 0.1
23import SystemSettings 1.0
24
25TextField {
26 id: field
27 property var next
28 anchors {
29 left: parent.left
30 right: parent.right
31 }
32 height: implicitHeight + units.gu(2)
33 style: TextFieldStyle {
34 overlaySpacing: units.gu(1)
35 frameSpacing: units.gu(1)
36 background: Rectangle {
37 property bool error: (field.hasOwnProperty("errorHighlight") &&
38 field.errorHighlight &&
39 !field.acceptableInput)
40 onErrorChanged: error ? UbuntuColors.orange : color
41 color: Theme.palette.selected.background
42 anchors.fill: parent
43 visible: field.activeFocus
44 }
45 color: UbuntuColors.lightAubergine
46 }
47
48 // Ubuntu.Keyboard
49 // TRANSLATORS: This is the text that will be used on the "return" key for the virtual keyboard,
50 // this word must be less than 5 characters
51 InputMethod.extensions: { "enterKeyText": i18n.tr("Next") }
52 KeyNavigation.tab: next
53 Keys.onReturnPressed: next.forceActiveFocus()
54}
055
=== added file 'plugins/cellular/Components/StandardAnimation.qml'
--- plugins/cellular/Components/StandardAnimation.qml 1970-01-01 00:00:00 +0000
+++ plugins/cellular/Components/StandardAnimation.qml 2015-07-27 14:21:15 +0000
@@ -0,0 +1,22 @@
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
17import QtQuick 2.2
18
19NumberAnimation {
20 duration: 300
21 easing.type: Easing.InOutQuad
22}
023
=== renamed file 'plugins/cellular/CustomApnEditor.qml' => 'plugins/cellular/PageApnEditor.qml'
--- plugins/cellular/CustomApnEditor.qml 2015-03-27 15:01:01 +0000
+++ plugins/cellular/PageApnEditor.qml 2015-07-27 14:21:15 +0000
@@ -3,7 +3,8 @@
3 *3 *
4 * Copyright (C) 2014 Canonical Ltd.4 * Copyright (C) 2014 Canonical Ltd.
5 *5 *
6 * Contact: Pat McGowan <pat.mcgowan@canonical.com>6 * Contact: Pat McGowan <pat.mcgowan@canonical.com>,
7 * Jonas G. Drange <jonas.drange@canonical.com>
7 *8 *
8 * This program is free software: you can redistribute it and/or modify it9 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published10 * under the terms of the GNU General Public License version 3, as published
@@ -19,367 +20,414 @@
19 */20 */
2021
21import QtQuick 2.022import QtQuick 2.0
22import QtQuick.Layouts 1.1
23import SystemSettings 1.023import SystemSettings 1.0
24import Ubuntu.Components 1.124import Ubuntu.Components 1.1
25import Ubuntu.Components.Popups 1.0
25import Ubuntu.Components.ListItems 1.0 as ListItem26import Ubuntu.Components.ListItems 1.0 as ListItem
26import "apn.js" as APN27import MeeGo.QOfono 0.2
28import "Components" as LocalComponents
29import "apn_editor.js" as Editor
2730
28ItemPage {31ItemPage {
29 objectName: "customapnPage"
30 id: root32 id: root
3133 objectName: "apnEditor"
32 // "internet" or "mms"34
33 property var type35 // Property that holds the APN manager (apn_manager.js) module.
3436 property var manager
35 // dict of "type" : ctx37
36 property var contexts;38 property OfonoContextConnection contextQML
3739
38 /// work around LP(#1361919)40 // All models.
39 property var activateCb;41 property ListModel mmsModel
4042 property ListModel internetModel
41 // Sim qml object43 property ListModel iaModel
42 property var sim44
4345 // Flags that indicate what context we are editing.
44 QtObject {46 property bool isCombo: Editor.indexToType(typeSel.selectedIndex) === 'internet+mms'
45 id: d47 property bool isInternet: Editor.indexToType(typeSel.selectedIndex) === 'internet'
46 property var typeText : type === "internet" ? i18n.tr("Internet") : i18n.tr("MMS")48 property bool isMms: Editor.indexToType(typeSel.selectedIndex) === 'mms'
47 property bool isMms : type === "mms"49 property bool isIa: Editor.indexToType(typeSel.selectedIndex) === 'ia'
4850
49 property bool isValid : false51 property bool isValid: Editor.isValid()
5052
51 function validateFields() {53 // priv
52 if (apnName.text === "") {54 property bool _edgeReady: false
53 isValid = false;55
54 return56 property QtObject activeItem: null
55 }57
56 if (isMms) {58 // When a user has requested saving a context.
57 if (mmsc.text === "") {59 signal saving ()
58 isValid = false;60
59 return;61 // When user has saved a context.
60 }62 signal saved ()
61 /// @todo validate proxy63
62 /// @todo force port to be integer and validate it's value64 // Signal for new contexts.
63 }65 signal newContext (OfonoContextConnection context)
6466
65 // @todo the rest67 // When user cancels.
66 isValid = true;68 signal canceled ()
67 }69
68 }70 title: contextQML ? i18n.tr("Edit") : i18n.tr("New APN")
6971
70 //TRANSLATORS: %1 is either i18n.tr("Internet") or i18n.tr("MMS")72 state: "default"
71 title: i18n.tr("Custom %1 APN").arg(d.typeText)73 states: [
7274 PageHeadState {
73 // workaround of getting the following error on startup:75 name: "default"
74 // WARNING - ... : QML Page: Binding loop detected for property "flickable"76 head: root.head
77 actions: [
78 Action {
79 objectName: "saveApn"
80 iconName: "ok"
81 enabled: isValid
82 onTriggered: Editor.saving()
83 }
84 ]
85 },
86 PageHeadState {
87 name: "busy"
88 head: root.head
89 actions: [
90 Action {
91 iconName: "ok"
92 enabled: false
93 }
94 ]
95 },
96 State {
97 name: "busy"
98 PropertyChanges { target: name; enabled: false; }
99 PropertyChanges { target: accessPointName; enabled: false; }
100 PropertyChanges { target: username; enabled: false; }
101 PropertyChanges { target: password; enabled: false; }
102 PropertyChanges { target: messageCenter; enabled: false; }
103 PropertyChanges { target: messageProxy; enabled: false; }
104 PropertyChanges { target: port; enabled: false; }
105 }
106 ]
107
108 onSaving: state = "busy"
109 onSaved: pageStack.pop();
110 onCanceled: pageStack.pop();
111 onNewContext: Editor.newContext(context);
112
113 Component.onCompleted: {
114 if (contextQML) {
115 Editor.populate(contextQML);
116 }
117 Editor.ready();
118 }
119
120 // We need to disable keyboard anchoring because we implement the
121 // KeyboardRectangle pattern
122 Binding {
123 target: main
124 property: "anchorToKeyboard"
125 value: false
126 }
127
75 flickable: null128 flickable: null
76 Component.onCompleted: {
77 flickable: scrollWidget
78
79 var ctx;
80 if (d.isMms) {
81 ctx = contexts["mms"];
82 if (ctx === undefined) {
83 // @bug LP(:#1362795)
84 return;
85 }
86 } else {
87 ctx = contexts["internet"]
88 }
89
90 // We do not have any context yet, we will create one, but not now.
91 // We create it once the user presses 'Activate'.
92 if (!ctx) {
93 return;
94 }
95
96 apnName.text = ctx.accessPointName;
97 userName.text = ctx.username;
98 pword.text = ctx.password;
99 mmsc.text = ctx.messageCenter;
100 var proxyText = ctx.messageProxy.split(":");
101 proxy.text = proxyText[0] !== undefined ? proxyText[0] : "";
102 port.text = proxyText[1] !== undefined ? proxyText[1] : "";
103 /// @todo protocol values
104
105 if (d.isMms) {
106 /// @todo disabled for now
107 doBoth.checked = false;
108 return;
109 var internetApn = contexts["internet"]
110 if (ctx.accessPointName === internetApn.accessPointName &&
111 ctx.username == internetApn.username &&
112 ctx.password == internetApn.password
113 /* auth + procol */) {
114 doBoth.checked = true;
115 }
116 else
117 doBoth.checked = false;
118 }
119 }
120
121 Flickable {129 Flickable {
122 id: scrollWidget130 id: scrollArea
131 objectName: "scrollArea"
132
133 // this is necessary to avoid the page to appear below the header
134 clip: true
135 flickableDirection: Flickable.VerticalFlick
123 anchors {136 anchors {
124 top: parent.top137 fill: parent
125 left: parent.left138 bottomMargin: keyboard.height
126 right: parent.right
127 bottom: parent.bottom
128 margins: units.gu(2)
129 }139 }
140 contentHeight: contents.height + units.gu(2)
130 contentWidth: parent.width141 contentWidth: parent.width
131 clip: true142
132 contentHeight: theContents.height143 // after add a new field we need to wait for the contentHeight to
133 boundsBehavior: (contentHeight > height) ? Flickable.DragAndOvershootBounds : Flickable.StopAtBounds144 // change to scroll to the correct position
134 flickableDirection: Flickable.VerticalFlick145 onContentHeightChanged: Editor.makeMeVisible(root.activeItem)
135146
136 ColumnLayout {147 Column {
137 id: theContents148 id: contents
138 anchors {149 anchors { left: parent.left; right: parent.right; }
139 left: parent.left150
140 right: parent.right151 // Type controls
141 }152 Column {
142 spacing: units.gu(2)153 anchors { left: parent.left; right: parent.right; }
143154
144 ListItem.Standard {155 SettingsItemTitle {
145 id: sameSwitch156 text: i18n.tr("Used for:")
146 anchors {
147 left: parent.left
148 right: parent.right
149 }157 }
150 /// @todo disable for now158
151 //visible: d.isMms159 ListItem.ItemSelector {
152 visible: false160 model: [i18n.tr('Internet and MMS'),
153 text: i18n.tr("Same APN as for Internet")161 i18n.tr('Internet'),
154 control: Switch {162 i18n.tr('MMS'),
155 id: doBoth163 i18n.tr('LTE'), ]
156 checked: false164 id: typeSel
157 anchors.verticalCenter: parent.verticalCenter165 objectName: "typeSelector"
158 onClicked: {166 delegate: OptionSelectorDelegate {
159 if (checked) {167 showDivider: false
160 var internetApn = contexts["internet"]168 objectName: "type_" + Editor.indexToType(index)
161 apnName.text = internetApn.accessPointName;
162 userName.text = internetApn.username;
163 pword.text = internetApn.password;
164 }
165 }169 }
166 }170 width: parent.width
167 }171 KeyNavigation.tab: name
168172 }
169 GridLayout {173 }
170 id: theGrid174
171 columns: 2175 // Name controls
172 columnSpacing: units.gu(1)176 Column {
173 rowSpacing: units.gu(1)177 anchors { left: parent.left; right: parent.right; }
174 anchors{178
175 right: parent.right179 SettingsItemTitle {
176 left:parent.left180 anchors { left: parent.left; right: parent.right }
177 }181 text: i18n.tr("Name")
178182 }
179 Label {183
180 //TRANSLATORS: %1 is either i18n.tr("Internet") or i18n.tr("MMS")184 LocalComponents.LabelTextField {
181 text: i18n.tr("%1 APN").arg(d.typeText)185 id: name
182 }186 objectName: "name"
183 TextField {187 inputMethodHints: Qt.ImhNoAutoUppercase |
184 id: apnName188 Qt.ImhNoPredictiveText
185 enabled: !doBoth.checked189 placeholderText: i18n.tr("Enter a name describing the APN")
186 onTextChanged: d.validateFields()190 next: accessPointName
187 inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText191 Component.onCompleted: forceActiveFocus()
188 }192 onActiveFocusChanged: if (activeFocus) Editor.makeMeVisible(
189193 name)
190 Label {194 }
195 }
196
197 // APN controls
198 Column {
199 anchors { left: parent.left; right: parent.right; }
200
201 SettingsItemTitle {
202 anchors { left: parent.left; right: parent.right }
203 /* TRANSLATORS: This string is a description of a text
204 field and should thus be concise. */
205 text: i18n.tr("APN")
206 }
207
208 LocalComponents.LabelTextField {
209 id: accessPointName
210 objectName: "accessPointName"
211 inputMethodHints: Qt.ImhUrlCharactersOnly |
212 Qt.ImhNoAutoUppercase |
213 Qt.ImhNoPredictiveText
214 placeholderText: i18n.tr("Enter the name of the access point")
215 next: isMms || isCombo ? messageCenter : username
216 onActiveFocusChanged: if (activeFocus) Editor.makeMeVisible(
217 accessPointName)
218 }
219 }
220
221 // MMSC controls
222 Column {
223 anchors { left: parent.left; right: parent.right }
224 visible: isMms || isCombo
225
226 SettingsItemTitle {
227 anchors { left: parent.left; right: parent.right }
191 text: i18n.tr("MMSC")228 text: i18n.tr("MMSC")
192 visible: d.isMms229 }
193 }230
194 TextField {231 LocalComponents.LabelTextField {
195 id: mmsc232 id: messageCenter
196 visible: d.isMms233 objectName: "messageCenter"
197 onTextChanged: d.validateFields()234 inputMethodHints: Qt.ImhUrlCharactersOnly |
198 inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText235 Qt.ImhNoAutoUppercase |
199 }236 Qt.ImhNoPredictiveText
200 Label {237 placeholderText: i18n.tr("Enter message center")
238 next: messageProxy
239 onFocusChanged: {
240 if (!focus && text.length > 0) {
241 text = Editor.setHttp(text);
242 }
243 if (activeFocus) Editor.makeMeVisible(messageCenter);
244 }
245 }
246 }
247
248 // Proxy controls
249 Column {
250 anchors { left: parent.left; right: parent.right }
251 visible: isMms || isCombo
252
253 SettingsItemTitle {
254 anchors { left: parent.left; right: parent.right }
201 text: i18n.tr("Proxy")255 text: i18n.tr("Proxy")
202 visible: d.isMms256 }
203 }257
204 TextField {258 LocalComponents.LabelTextField {
205 id: proxy259 id: messageProxy
206 visible: d.isMms260 objectName: "messageProxy"
207 onTextChanged: d.validateFields()261 inputMethodHints: Qt.ImhUrlCharactersOnly |
208 inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText262 Qt.ImhNoAutoUppercase |
209 }263 Qt.ImhNoPredictiveText
210 Label {264 placeholderText: i18n.tr("Enter message proxy")
211 text: i18n.tr("Port")265 next: port
212 visible: d.isMms266 onTextChanged: {
213 }267 movePortDelay.running = false;
214 TextField {268 if (text.search(/\:\d+$/) >= 0) {
269 movePortDelay.running = true;
270 }
271 }
272 onActiveFocusChanged: if (activeFocus) Editor.makeMeVisible(
273 messageProxy)
274 }
275
276 Timer {
277 id: movePortDelay
278 interval: 1000
279 onTriggered: {
280
281 function getPort(s) {
282 var match = s.match(/\:(\d+)/);
283 if (match === null) {
284 return null;
285 } else {
286 return match[1];
287 }
288 }
289
290 var prt = getPort(messageProxy.text);
291 var portIndex = messageProxy.text.indexOf(prt);
292 var textSansPort = messageProxy.text.slice(0, portIndex - 1);
293
294 if (prt) {
295 messageProxy.text = textSansPort;
296 port.text = prt;
297 }
298 }
299 }
300 }
301
302 // Proxy port controls
303 Column {
304 anchors { left: parent.left; right: parent.right }
305 visible: isMms || isCombo
306
307 SettingsItemTitle {
308 id: portLabel
309 text: i18n.tr("Proxy port")
310 }
311
312 LocalComponents.LabelTextField {
215 id: port313 id: port
216 visible: d.isMms314 objectName: "port"
217 maximumLength: 4315 maximumLength: 5
218 onTextChanged: d.validateFields()316 inputMethodHints: Qt.ImhNoAutoUppercase |
219 inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText317 Qt.ImhNoPredictiveText
220 }318 validator: portValidator
221319 placeholderText: i18n.tr("Enter message proxy port")
222 Label {320 next: username
223 text: i18n.tr("Username")321 onActiveFocusChanged: if (activeFocus) Editor.makeMeVisible(
224 }322 port)
225 TextField {323 }
226 id: userName324
227 enabled: !doBoth.checked325 RegExpValidator {
228 inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText326 id: portValidator
229 }327 regExp: /([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])/
230328 }
231 Label {329 }
330
331 // Username controls
332 Column {
333 anchors { left: parent.left; right: parent.right }
334
335 SettingsItemTitle {
336 width: parent.width
337 text: i18n.tr("User name")
338 }
339
340 LocalComponents.LabelTextField {
341 id: username
342 objectName: "username"
343 inputMethodHints: Qt.ImhNoAutoUppercase |
344 Qt.ImhNoPredictiveText |
345 Qt.ImhSensitiveData
346 placeholderText: i18n.tr("Enter username")
347 next: password
348 onActiveFocusChanged: if (activeFocus) Editor.makeMeVisible(
349 username)
350 }
351 }
352
353 // Password controls
354 Column {
355 anchors { left: parent.left; right: parent.right }
356
357 SettingsItemTitle {
358 width: parent.width
232 text: i18n.tr("Password")359 text: i18n.tr("Password")
233 }360 }
234 TextField {361
235 id: pword362 LocalComponents.LabelTextField {
236 enabled: !doBoth.checked363 id: password
237 inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText364 objectName: "password"
238 }365 width: parent.width
239 /// @todo support for ipv6 will be added after RTM366 echoMode: TextInput.Normal
240 }367 inputMethodHints: Qt.ImhNoAutoUppercase |
241368 Qt.ImhNoPredictiveText |
242 Item {369 Qt.ImhSensitiveData
243 id: buttonRectangle370 placeholderText: i18n.tr("Enter password")
244371 next: name
245 height: cancelButton.height + units.gu(2)372 onActiveFocusChanged: if (activeFocus) Editor.makeMeVisible(
246373 password)
247 anchors {374 }
248 left: parent.left375 }
249 right: parent.right376
250 }377 // Authentication controls
251378 Column {
252 Button {379 anchors { left: parent.left; right: parent.right }
253 id: cancelButton380 visible: showAllUI
254381
255 text: i18n.tr("Cancel")382 SettingsItemTitle {
256383 width: parent.width
257 anchors {384 text: i18n.tr("Authentication")
258 left: parent.left385 }
259 right: parent.horizontalCenter386
260 bottom: parent.bottom387 ListItem.ItemSelector {
261 topMargin: units.gu(1)388 model: [i18n.tr("None"),
262 rightMargin: units.gu(1)389 i18n.tr("PAP or CHAP"),
263 bottomMargin: units.gu(1)390 i18n.tr("PAP only"),
264 }391 i18n.tr("CHAP only")]
265392 }
266 onClicked: {393 }
267 pageStack.pop()394
268 }395 // Protocol controls
269 }396 Column {
270397 anchors { left: parent.left; right: parent.right }
271 Button {398 visible: showAllUI
272 id: confirmButton399
273400 SettingsItemTitle {
274 text: d.isMms ? i18n.tr("Save") : i18n.tr("Activate")401 width: parent.width
275402 text: i18n.tr("Protocol")
276 anchors {403 }
277 left: parent.horizontalCenter404
278 right: parent.right405 ListItem.ItemSelector {
279 bottom: parent.bottom406 model: [i18n.tr("IPv4"),
280 topMargin: units.gu(1)407 i18n.tr("IPv6"),
281 leftMargin: units.gu(1)408 i18n.tr("IPv4v6")]
282 rightMargin: units.gu(4)409 }
283 bottomMargin: units.gu(1)410 }
284 }411 }
285412 } // main column holding all controls and buttons
286 enabled: d.isValid;413
287414 Timer {
288 onClicked: {415 id: updateContext
289 var ctx;416 property OfonoContextConnection ctx
290 var values;417 interval: 1500
291418 onTriggered: {
292 if (d.isMms)419 Editor.updateContextQML(ctx);
293 ctx = contexts["mms"];420 root.saved();
294 else421 }
295 ctx = contexts["internet"];422 }
296423
297 /// @bug LP(:#1362795)424 LocalComponents.KeyboardRectangle {
298 if (d.isMms && ctx === undefined) {425 id: keyboard
299 var mmsData = ({})426 anchors.bottom: parent.bottom
300 mmsData["accessPointName"] = apnName.text;427 onHeightChanged: {
301 mmsData["username"] = userName.text;428 if (root.activeItem) {
302 mmsData["password"] = pword.text;429 Editor.makeMeVisible(root.activeItem);
303 mmsData["messageCenter"] = mmsc.text430 }
304 var proxyValue = "";431 }
305 if (proxy.text !== "") {432 }
306 proxyValue = proxy.text;
307 if (port.text !== "")
308 proxyValue = proxyValue + ":" + port.text;
309 }
310 mmsData["messageProxy"] = proxyValue;
311 activateCb("mms", undefined, mmsData);
312 pageStack.pop();
313 return;
314 }
315
316 values = {
317 'accessPointName': apnName.text,
318 'username': userName.text,
319 'password': pword.text
320 };
321
322 if (d.isMms) {
323 values['messageCenter'] = mmsc.text;
324 var proxyValue = "";
325 if (proxy.text !== "") {
326 proxyValue = proxy.text;
327 if (port.text !== "")
328 proxyValue = proxyValue + ":" + port.text;
329 }
330 values['messageProxy'] = proxyValue;
331 }
332
333 // If we are editing an existing context, update
334 // its values, activate and exit.
335 if (ctx) {
336 APN.updateContext(ctx, values);
337 activateCb(ctx.type, ctx.contextPath);
338 pageStack.pop();
339 } else {
340 // If we do not have a context, create one and defer
341 // editing it to when it has been created—and we
342 // will also wait until Ofono has given it a name.
343 // This is all very async, so we will use connect()
344 // and dynamic QML.
345
346 function addedCustomContext (path) {
347 // This is a handler for when we add a context
348 // in the custom apn editor.
349 // We create a temporary QML object for the
350 // context like this, until we get the APN
351 // editor refactored.
352 // TODO(jgdx): create some kind of libqofono
353 // objects framework in apn.js
354 var newCtx = Qt.createQmlObject(
355 'import MeeGo.QOfono 0.2;'+
356 'OfonoContextConnection {'+
357 'contextPath: "'+path+'" }',
358 root, "apn.js");
359
360 // Ofono sets a default name (Internet or MMS).
361 // We are going to change it to our default name.
362 newCtx.nameChanged.connect(function (name) {
363 if (name === "Internet") {
364 newCtx.name = APN.CUSTOM_INTERNET_CONTEXT_NAME();
365 APN.updateContext(newCtx, values);
366 } else if (name === "MMS") {
367 newCtx.name = APN.CUSTOM_MMS_CONTEXT_NAME();
368 APN.updateContext(newCtx, values);
369 } else {
370 newCtx.destroy(100);
371 activateCb(newCtx.type, newCtx.contextPath);
372 pageStack.pop();
373 }
374 });
375 }
376
377 sim.connMan.addContext(root.type);
378 sim.connMan.contextAdded.connect(addedCustomContext);
379 }
380 }
381 }
382 } // item for buttons
383 } // the contents
384 } // the flickable
385}433}
386434
=== modified file 'plugins/cellular/PageChooseApn.qml'
--- plugins/cellular/PageChooseApn.qml 2015-03-27 15:01:01 +0000
+++ plugins/cellular/PageChooseApn.qml 2015-07-27 14:21:15 +0000
@@ -3,7 +3,8 @@
3 *3 *
4 * Copyright (C) 2014 Canonical Ltd.4 * Copyright (C) 2014 Canonical Ltd.
5 *5 *
6 * Contact: Pat McGowan <pat.mcgowan@canonical.com>6 * Contact: Pat McGowan <pat.mcgowan@canonical.com>,
7 * Jonas G. Drange <jonas.drange@canonical.com>
7 *8 *
8 * This program is free software: you can redistribute it and/or modify it9 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published10 * under the terms of the GNU General Public License version 3, as published
@@ -16,15 +17,20 @@
16 *17 *
17 * You should have received a copy of the GNU General Public License along18 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.19 * with this program. If not, see <http://www.gnu.org/licenses/>.
20 *
21 * Note: Everything user facing refers to APN and e.g. LTE, but in the
22 * code an APN configuration is a 'context' and LTE is 'ia'.
23 *
19 */24 */
2025
21import QtQuick 2.026import QtQuick 2.0
22import SystemSettings 1.027import SystemSettings 1.0
23import Ubuntu.Components 1.128import Ubuntu.Components 1.1
24import Ubuntu.Components.Popups 0.129import Ubuntu.Components.Popups 1.0
25import Ubuntu.Components.ListItems 0.1 as ListItem30import Ubuntu.Components.ListItems 1.0 as ListItem
26import MeeGo.QOfono 0.231import MeeGo.QOfono 0.2
27import Ubuntu.SystemSettings.Cellular 1.032import Ubuntu.SystemSettings.Cellular 1.0
33import "apn_manager.js" as Manager
2834
29ItemPage {35ItemPage {
30 id: root36 id: root
@@ -32,548 +38,340 @@
32 objectName: "apnPage"38 objectName: "apnPage"
3339
34 property var sim40 property var sim
3541 property var editor
36 QtObject {42
37 id: d43 // Signal that indicates that we have all our contexts.
3844 signal ready()
39 // map of contextPath : connCtx45 Component.onCompleted: root.ready.connect(Manager.ready)
40 property var mContexts: ({})46
4147 /**
42 readonly property string mCustomContextNameInternet: "___ubuntu_custom_apn_internet"48 * We have three ListModels: one for Internet contexts, one for MMS
43 readonly property string mCustomContextNameMms: "___ubuntu_custom_apn_mms"49 * contexts and one for ia contexts. We use OfonoContextConnection qml
44 property var mCustomContextInternet : undefined50 * objects to represents the contexts.
45 property var mCustomContextMms : undefined51 *
4652 * The model will have helpful properties:
47 // LP(:#1362795)53 * title: Title that goes in the editor.
48 property var pendingCustomMmsData : ({})54 * type: A code that tells us what context type this model will have.
4955 *
50 // suppress any actions that we don't want to take56 * Model objects will have the following roles:
51 // when updating selectedIndexes, etc57 * path: the path of the context
52 property bool __suppressActivation : true;58 * qml: the QML of the context
53 property bool __haveCustomContexts : false;59 */
5460
55 function updateContexts(contexts)61 ListModel {
56 {62 id: mmsContexts
57 var tmp = contexts || sim.connMan.contexts.slice(0);63 property string title: i18n.tr("MMS APN")
58 var added = tmp.filter(function(i) {64 property string type: 'mms'
59 return mContexts[i] === undefined;65 }
60 });66
61 var removed = Object.keys(mContexts).filter(function(i) {67 ListModel {
62 return tmp.indexOf(i) === -1;68 id: internetContexts
63 })69 property string title: i18n.tr("Internet APN")
6470 property string type: 'internet'
65 removed.forEach(function(currentValue, index, array) {71 }
66 // just asserting to verify the logic72
67 // remove once proven functional73 ListModel {
68 // the exception gives a very nice log error message from QmlEngine and does not74 id: iaContexts
69 // have any side effects75 property string title: i18n.tr("LTE APN")
70 if (mContexts[currentValue] === undefined) {76 property string type: 'ia'
71 throw "updateContexts: removed is broken";
72 }
73
74 if (mContexts[currentValue].name === mCustomContextNameInternet)
75 mCustomContextInternet = undefined
76 else if (mContexts[currentValue].name === mCustomContextNameMms)
77 mCustomContextMms = undefined
78
79 mContexts[currentValue].destroy();
80 delete mContexts[currentValue];
81 });
82
83 added.forEach(function(currentValue, index, array) {
84 // just asserting to verify the logic
85 // remove once proven functional
86 // the exception gives a very nice log error message from QmlEngine and does not
87 // have any side effects
88 if (mContexts[currentValue] !== undefined) {
89 throw "updateContexts: added is broken";
90 }
91 var ctx = connCtx.createObject(parent,
92 {
93 "contextPath": currentValue
94 });
95 mContexts[currentValue] = ctx;
96 });
97
98 // just asserting to verify the logic
99 // remove once proven functional
100 // the exception gives a very nice log error message from QmlEngine and does not
101 // have any side effects
102 if (Object.keys(mContexts).length !== tmp.length) {
103 throw "Object.keys(contexts).length !== tmp.length";
104 }
105 tmp.forEach(function(currentValue, index, array) {
106 if (mContexts[currentValue] === undefined)
107 throw "contexts[currentValue] === undefined";
108 });
109
110 buildLists();
111 }
112
113 // expects updateContexts() to have ran before executing.
114 function checkAndCreateCustomContexts() {
115
116 // When a context is added, we assume it is a custom one.
117 function addedCustomContext (path) {
118
119 // We do not have a QML object representing this context,
120 // so we ask updateContexts to create one.
121 if (!d.mContexts[path]) {
122 d.updateContexts();
123 }
124 d.mContexts[path].name = mCustomContextNameInternet;
125 sim.connMan.contextAdded.disconnect(addedCustomContext);
126 }
127
128 var customInternet = Object.keys(mContexts).filter(function (i) {
129 var ctx = mContexts[i];
130 return ctx.name === mCustomContextNameInternet;
131 });
132 var customMms = Object.keys(mContexts).filter(function (i) {
133 var ctx = mContexts[i];
134 return ctx.name === mCustomContextNameMms;
135 });
136
137 // make sure there is only one context per type
138 if (customInternet.length > 1) {
139 customInternet.forEach(function(currentValue, index, array) {
140 if (index === 0)
141 return;
142 sim.connMan.removeContext(currentValue);
143 });
144 }
145 if (customMms.length > 1) {
146 customMms.forEach(function(currentValue, index, array) {
147 if (index === 0)
148 return;
149 sim.connMan.removeContext(currentValue);
150 });
151 }
152
153 if (customInternet.length !== 0) {
154 d.__haveCustomContexts = true;
155 }
156
157 // @bug don't create the custom MMS context
158 // LP(:#1362795)
159 // if (customMms.length === 0) {
160 // sim.connMan.addContext("mms");
161 // }
162
163 buildLists();
164 }
165
166 property var mInternetApns : [];
167 property var mMmsApns : [];
168 function buildLists()
169 {
170 d.__suppressActivation = true;
171
172 var internet = [];
173 var mms = [];
174
175 internet = Object.keys(mContexts).filter(function(i) {
176 var ctx = mContexts[i];
177 if (ctx.type === "internet") {
178 if (ctx.name === mCustomContextNameInternet) {
179 mCustomContextInternet = ctx
180 // don't add yet
181 return false;
182 }
183 return true;
184 }
185 return false;
186 });
187 mms = Object.keys(mContexts).filter(function(i) {
188 var ctx = mContexts[i];
189 if ( ctx.type === "mms") {
190 if (ctx.name === mCustomContextNameMms) {
191 mCustomContextMms = ctx;
192 // don't add yet
193 return false;
194 }
195 return true;
196 }
197 return false;
198 });
199
200 // make sure customized are the last on the lists
201 if (mCustomContextInternet !== undefined)
202 internet = internet.concat([mCustomContextInternet.contextPath])
203 if (mCustomContextMms !== undefined)
204 mms = mms.concat([mCustomContextMms.contextPath])
205 else {
206 /// @bug LP(#1361864)
207 // add anyway a "dummy" Custom so we can show at least the one provisioned
208 // MMS context as long as the user does not hit "Custom" in the MMS list.
209 mms = mms.concat(["dummycustom"])
210 }
211
212 // add "Same APN as for Internet" to be the first on the MMS list
213 mms = ["/same/as/internet"].concat(mms);
214
215 mInternetApns = internet;
216 mMmsApns = mms;
217
218 internetApnSelector.updateSelectedIndex();
219
220 d.__suppressActivation = false;
221 }
222
223 function openApnEditor(type) {
224 var ctx;
225 if (type === "internet") {
226 ctx = mCustomContextInternet;
227 } else if (type == "mms") {
228 ctx = mCustomContextMms;
229 }
230 /// can't modify active context
231 if (ctx !== undefined && ctx.active)
232 ctx.active = false;
233
234 pageStack.push(Qt.resolvedUrl("CustomApnEditor.qml"),
235 {
236 type: type,
237 contexts: {"internet": mCustomContextInternet,
238 "mms": mCustomContextMms},
239 activateCb: activateHelper,
240 sim: sim
241 });
242 }
243
244 function activateHelper(type, contextPath, customMmsData) {
245 if (type === "internet") {
246 activator.activate(contextPath, sim.simMng.subscriberIdentity, sim.simMng.modemPath)
247 }
248 else if (type === "mms") {
249 if (contextPath === undefined) {
250 // LP(:#1362795)
251 pendingCustomMmsData = customMmsData
252 /// remove any provisioned ones..
253 var remove = []
254 Object.keys(mContexts).forEach(function(currentValue, index, array) {
255 var ctx = mContexts[currentValue];
256 if (ctx.type === "mms")
257 remove = remove.concat([ctx.contextPath])
258 });
259 remove.forEach(function(currentValue, index, array) {
260 sim.connMan.removeContext(currentValue);
261 });
262 sim.connMan.addContext("mms")
263 }
264 } else {
265 // somebody is calling the function wrong
266 // the exception gives a very nice log error message from QmlEngine and does not
267 // have any side effects
268 throw "activateHelper: bad data: type: " + type + ", contextPath: " + contextPath + ", customMmsData: " + customMmsData;
269 }
270 }
271 }
272
273 OfonoActivator {
274 id:activator
275 }77 }
27678
277 Component {79 Component {
278 id: connCtx80 id: contextComponent
279 OfonoContextConnection {81 OfonoContextConnection {
28082 property bool isCombined: type === 'internet' && messageCenter
281 // add helper property to detect dual internet/MMS contexts83 property string typeString: {
282 property bool dual : false84 if (isCombined) {
283 onTypeChanged:{85 return i18n.tr("Internet and MMS");
284 if (type == "internet")86 } else if (type === 'internet' && !messageCenter) {
285 if (messageCenter !== "")87 return i18n.tr("Internet");
286 dual = true88 } else if (type === 'ia') {
28789 return i18n.tr("LTE");
288 d.buildLists();90 } else if (type === 'mms') {
289 }91 return i18n.tr("MMS");
29092 } else {
291 onActiveChanged: if (type === "internet") internetApnSelector.updateSelectedIndex()93 return type;
292 onNameChanged: {94 }
29395 }
294 if (name === d.mCustomContextNameInternet) {96 }
295 d.__haveCustomContexts = true;97 }
296 }98
29799 state: "default"
298 if (name === "MMS") {100 states: [
299 this.name = d.mCustomContextNameMms;101 PageHeadState {
300 this.accessPointName = d.pendingCustomMmsData["accessPointName"];102 name: "default"
301 this.username = d.pendingCustomMmsData["username"];103 head: root.head
302 this.password = d.pendingCustomMmsData["password"];104 actions: [
303 this.messageCenter = d.pendingCustomMmsData["messageCenter"];105 Action {
304 this.messageProxy = d.pendingCustomMmsData["messageProxy"];106 iconName: "add"
305 d.pendingCustomMmsData = ({});107 objectName: "newApn"
306 }108 onTriggered: {
307 d.buildLists();109 editor = pageStack.push(Qt.resolvedUrl("PageApnEditor.qml"), {
308 }110 manager: Manager,
309 onAccessPointNameChanged: d.buildLists()111 mmsModel: mmsContexts,
310 onReportError: console.error("Context error on " + contextPath + ": " + errorString)112 internetModel: internetContexts,
311 }113 iaModel: iaContexts
312 }114 });
313115 }
314 Connections {116 }
315 target: sim.connMan117 ]
316 onContextsChanged: d.updateContexts(contexts)118 }
317 Component.onCompleted: {119 ]
318 /// @todo workaround the work around that the UI currently only supports max. 1 MMS context
319 /// remove once nuntium stuff is implemented
320 // remove all but one MMS context
321 var mms = Object.keys(d.mContexts).filter(function(i) {
322 var ctx = d.mContexts[i]
323 if (ctx.type === "mms")
324 return true;
325 return false;
326 });
327 mms = mms.slice(1);
328 mms.forEach(function(currentValue, index, array) {
329 sim.connMan.removeContext(currentValue);
330 });
331
332 d.updateContexts();
333 }
334 }
335120
336 Flickable {121 Flickable {
337 id: scrollWidget122 id: scrollWidget
338 anchors.fill: parent123 anchors.fill: parent
124 contentWidth: parent.width
339 contentHeight: contentItem.childrenRect.height125 contentHeight: contentItem.childrenRect.height
340 boundsBehavior: (contentHeight > root.height) ? Flickable.DragAndOvershootBounds : Flickable.StopAtBounds126 boundsBehavior: (contentHeight > root.height) ?
341 flickableDirection: Flickable.VerticalFlick127 Flickable.DragAndOvershootBounds : Flickable.StopAtBounds
342
343128
344 Column {129 Column {
345 anchors { left: parent.left; right: parent.right }130 id: apnsCol
346131 anchors { left: parent.left; right: parent.right }
347 spacing: units.gu(2)132
348133 Repeater {
349 ListItem.ItemSelector {134 id: apns
350 id: internetApnSelector135 anchors { left: parent.left; right: parent.right }
351 width: parent.width136 height: childrenRect.height
352 model: d.mInternetApns137 model: [internetContexts, mmsContexts, iaContexts]
353 expanded: true138 delegate: apnsDelegate
354 selectedIndex: -1139 }
355 text: i18n.tr("Internet APN:")140 }
356 onModelChanged: updateSelectedIndex()141
357142 Button {
358 function updateSelectedIndex()143 anchors {
359 {144 top: apnsCol.bottom
360 var tmp = d.__suppressActivation145 right: parent.right
361 d.__suppressActivation = true;146 left: parent.left
362 var idx = -1;147 margins: units.gu(2)
363 if (model) {148 }
364 model.forEach(function(currentValue, index, array) {149 text: i18n.tr("Reset All APN Settings…")
365 if (d.mContexts[currentValue].active)150 onClicked: PopupUtils.open(resetDialog)
366 idx = index;151 }
367 });152 }
368 }153
369 selectedIndex = idx;154 Component {
370 d.__suppressActivation = tmp;155 id: resetDialog
371 }156 Dialog {
372157 id: dialogue
373 delegate: OptionSelectorDelegate {158 title: i18n.tr("Reset APN Settings")
374 showDivider: false159 text: i18n.tr("Are you sure that you want to Reset APN Settings?")
375 text: {160
376 var ctx = d.mContexts[modelData];161 Button {
377 if (ctx.name !== "") {162 text: i18n.tr("Reset")
378 if (ctx.name !== d.mCustomContextNameInternet) {163 color: UbuntuColors.orange
379 return ctx.name164 onClicked: {
380 } else {165 Manager.reset();
381 //: user visible name of the custom Internet APN166 PopupUtils.close(dialogue);
382 return i18n.tr("Custom");167 }
383 }168 }
384 } else {169
385 return ctx.accessPointName170 Button {
386 }171 text: i18n.tr("Cancel")
387 }172 onClicked: PopupUtils.close(dialogue)
388 }173 }
389 onSelectedIndexChanged: {174 }
390 if (selectedIndex === -1) {175 }
391 if (mmsApnSelector && mmsApnSelector.model[mmsApnSelector.selectedIndex] === "/same/as/internet")176
392 mmsApnSelector.selectedIndex = -1;177 Component {
393 return;178 id: disablesInternetWarning
394 }179 Dialog {
395180 id: dialogue
396 var ctx = d.mContexts[model[selectedIndex]];181 property OfonoContextConnection mms
397 if(ctx.dual) {182 property OfonoContextConnection combined
398 if (!d.mCustomContextMms)183 /* TRANSLATORS: %1 is the MMS APN that the user has chosen to be
399 mmsApnSelector.selectedIndex = mmsApnSelector.model.indexOf("/same/as/internet");184 “preferred”. */
400 }185 title: i18n.tr("Prefer %1").arg(mms.name)
401 else if (mmsApnSelector.model[mmsApnSelector.selectedIndex] === "/same/as/internet")186 /* TRANSLATORS: %1 is the MMS APN that the user has chosen to be
402 mmsApnSelector.selectedIndex = -1;187 “preferred”, i.e. used to retrieve MMS messages. %2 is the Internet
403188 APN that will be “de-preferred” as a result of this action. */
404 if (d.__suppressActivation)189 text: i18n.tr("You have chosen %1 as your preferred MMS APN. " +
405 return;190 "This disconnects an Internet connection provided " +
406191 "by %2.").arg(mms.name).arg(combined.name)
407 if (d.mCustomContextInternet && model[selectedIndex] === d.mCustomContextInternet.contextPath) {192
408 if (d.mCustomContextInternet.accessPointName === "") {193 Button {
409 d.openApnEditor("internet");194 text: i18n.tr("Disconnect")
410 updateSelectedIndex();195 onClicked: {
411 return;196 Manager.setPreferred(mms, true, true);
412 }197 PopupUtils.close(dialogue);
413 }198 }
414199 }
415 d.activateHelper("internet", ctx.contextPath);200
416 }201 Button {
417 }202 text: i18n.tr("Cancel")
418203 onClicked: PopupUtils.close(dialogue)
419204 }
420 Button {205 }
421 anchors { horizontalCenter: parent.horizontalCenter }206 }
422 objectName: "customApnEdit"207
423 text: i18n.tr("Custom Internet APN…")208 Component {
424 width: parent.width - units.gu(4)209 id: disablesMMSWarning
425 onClicked: d.openApnEditor("internet")210 Dialog {
426 }211 id: dialogue
427212 property OfonoContextConnection internet
428 ListItem.ItemSelector {213 property OfonoContextConnection combined
429 id: mmsApnSelector214 /* TRANSLATORS: %1 is the Internet APN that the user has chosen to
430 width: parent.width215 be “preferred”. */
431 model: d.mMmsApns216 title: i18n.tr("Prefer %1").arg(internet.name)
432 expanded: true217 /* TRANSLATORS: %1 is the Internet APN that the user has chosen to
433 selectedIndex: -1218 be “preferred”, i.e. used to connect to the Internet. %2 is the MMS
434 text: i18n.tr("MMS APN:")219 APN that will be “de-preferred” as a result of this action. */
435 delegate: OptionSelectorDelegate {220 text: i18n.tr("You have chosen %1 as your preferred Internet APN. " +
436 showDivider: modelData === "/same/as/internet"221 "This disables the MMS configuration provided " +
437 enabled: {222 "by %2.").arg(internet.name).arg(combined.name)
438 if (modelData !== "/same/as/internet")223
439 return true;224 Button {
440 else {225 text: i18n.tr("Disable")
441 var tmp = d.mContexts[internetApnSelector.model[internetApnSelector.selectedIndex]]226 onClicked: {
442 return tmp === undefined ? false : tmp.dual227 Manager.setPreferred(internet, true, true);
443 }228 PopupUtils.close(dialogue);
444 }229 }
445 // work around OptionSelectorDelegate not having a visual change depending on being disabled230 }
446 opacity: enabled ? 1.0 : 0.5231
447 text: {232 Button {
448 if (modelData === "/same/as/internet") {233 text: i18n.tr("Cancel")
449 return i18n.tr("Same APN as for Internet");234 onClicked: PopupUtils.close(dialogue)
450 }235 }
451 if (modelData === "dummycustom") {236 }
452 return i18n.tr("Custom");237 }
453 }238
454 var ctx = d.mContexts[modelData];239 Component {
455 if (ctx.name !== "") {240 id: disableContextWarning
456 if (ctx.name !== d.mCustomContextNameMms) {241 Dialog {
457 return ctx.name242 id: dialogue
458 } else {243 property OfonoContextConnection context
459 //: user visible name of the custom MMS APN244 /* TRANSLATORS: %1 is the APN that the user has disconnected or
460 return i18n.tr("Custom");245 disabled. */
461 }246 title: context.type === 'internet' ?
462 } else {247 i18n.tr("Disconnect %1").arg(context.name) :
463 return ctx.accessPointName248 i18n.tr("Disable %1").arg(context.name)
464 }249
465 }250 /* TRANSLATORS: %1 is the APN that the user has disconnected or
466 }251 disabled. */
467 onModelChanged: updateSelectedIndex();252 text: context.type === 'internet' ?
468 function updateSelectedIndex()253 i18n.tr("This disconnects %1.").arg(context.name) :
469 {254 i18n.tr("This disables %1.").arg(context.name)
470 // if we have custom MMS context, it must be active.255
471 // @bug LP(#1361864)256 Button {
472 var tmp = d.__suppressActivation;257 text: context.type === 'mms' ?
473 d.__suppressActivation = true;258 i18n.tr("Disable") :
474 if (d.mCustomContextMms) {259 i18n.tr("Disconnect")
475 selectedIndex = model.indexOf(d.mCustomContextMms.contextPath);260 onClicked: {
476 } else if (model.length === 3) {261 Manager.setPreferred(context, false, true);
477 /* meaning we have:262 PopupUtils.close(dialogue);
478 * 0 - /same/as/internet263 }
479 * 1 - some provisioned one264 }
480 * 2 - dummycustom265
481 */266 Button {
482 selectedIndex = 1;267 text: i18n.tr("Cancel")
483 } else if (internetApnSelector.model && internetApnSelector.selectedIndex !== -1) {268 onClicked: PopupUtils.close(dialogue)
484 if (d.mContexts[internetApnSelector.model[internetApnSelector.selectedIndex]].dual) {269 }
485 selectedIndex = model.indexOf("/same/as/internet");270 }
486 } else271 }
487 selectedIndex = -1;272
488 } else {273 Component {
489 selectedIndex = -1;274 id: apnsDelegate
490 }275
491 d.__suppressActivation = tmp;276 Repeater {
492 }277 anchors { left: parent.left; right: parent.right }
493278 model: modelData
494 onSelectedIndexChanged: {279 delegate: apnDelegate
495 if (selectedIndex === -1 || d.__suppressActivation)280 }
496 return;281 }
497282
498 if (model[selectedIndex] === "/same/as/internet") {283 Component {
499 // @bug delete _any_ actual MMS context284 id: apnDelegate
500 // LP:(#1362795)285
501 var remove = [];286 ListItem.Standard {
502 Object.keys(d.mContexts).forEach(function(currentValue, index, array) {287 id: apnListItem
503 var ctx = d.mContexts[currentValue];288 property alias text: apnItemName.text
504 if (ctx.type === "mms")289 objectName: "edit_" + qml.name
505 remove = remove.concat([ctx.contextPath]);290 height: units.gu(6)
506 });291 removable: true
507 remove.forEach(function(currentValue, index, array) {292 confirmRemoval: true
508 sim.connMan.removeContext(currentValue);293 progression: true
509 });294
510 return;295 onItemRemoved: Manager.removeContext(path);
511 }296 onClicked: {
512297 editor = pageStack.push(Qt.resolvedUrl("PageApnEditor.qml"), {
513 if (model[selectedIndex] === "dummycustom") {298 manager: Manager,
514 d.openApnEditor("mms");299 contextQML: qml,
515 updateSelectedIndex()300 mmsModel: mmsContexts,
516 return;301 internetModel: internetContexts,
517 }302 iaModel: iaContexts
518303 });
519 // no need to "activate" anything.304 }
520 // just fall through return here.305
521 // once we actually are able to suppport multiple MMS contexts306 control: CheckBox {
522 // on the system, add some code here to set one of them active307 id: check
523 }308 objectName: qml.name + "_preferred"
524 }309 property bool serverChecked: qml && qml.preferred
525310 onServerCheckedChanged: checked = serverChecked
526 Button {311 Component.onCompleted: checked = serverChecked
527 anchors { horizontalCenter: parent.horizontalCenter }312 onTriggered: Manager.setPreferred.call(this, qml, checked)
528 objectName: "customApnEdit"313 }
529 text: i18n.tr("Custom MMS APN…")314
530 width: parent.width - units.gu(4)315 Item {
531 onClicked: {316 anchors {
532 if (!d.__haveCustomContexts) {317 top: parent.top
533 d.checkAndCreateCustomContexts();318 bottom: parent.bottom
534 d.__haveCustomContexts = true;319 left: parent.left
535 }320 leftMargin: units.gu(2)
536 d.openApnEditor("mms");321 right: parent.right
537 }322 }
538 }323
539324 Label {
540 // @todo: no means of doing any meaningful reset right now.325 id: apnItemName
541 // LP(#1338758)326 anchors {
542 // ListItem.ThinDivider {}327 topMargin: units.gu(1)
543 // ListItem.SingleControl {328 top: parent.top
544 // control: Button {329 left: parent.left
545 // objectName: "resetButton"330 right: parent.right
546 // text: i18n.tr("Reset APN Settings")331 }
547 // width: parent.width - units.gu(4)332
548 // onClicked: {333 text: qml.name
549 // PopupUtils.open(resetDialog)334 elide: Text.ElideRight
550 // }335 opacity: apnListItem.enabled ? 1.0 : 0.5
551 // }336 }
552 // }337
553 }338 Label {
554 }339 id: apnItemType
555340 anchors {
556 Component {341 left: parent.left
557 id: resetDialog342 right: parent.right
558 Dialog {343 top: apnItemName.bottom
559 id: dialogue344 }
560 title: i18n.tr("Reset APN Settings")345
561 text: i18n.tr("Are you sure that you want to Reset APN Settings?")346 text: qml.typeString
562 Button {347 color: Theme.palette.normal.backgroundText
563 text: i18n.tr("Cancel")348 fontSize: "small"
564 onClicked: PopupUtils.close(dialogue)349 wrapMode: Text.Wrap
565 }350 maximumLineCount: 5
566 Button {351 }
567 text: i18n.tr("Reset")352 }
568 color: UbuntuColors.orange353 }
569 onClicked: {354 }
570 // delete all APNs355
571 // kick ofono per356 Connections {
572 // https://bugs.launchpad.net/ubuntu/+source/ofono/+bug/1338758357 target: sim.connMan
573358
574 }359 onContextAdded: Manager.contextAdded(path)
575 }360 onContextRemoved: Manager.contextRemoved(path)
576361 onContextsChanged: Manager.contextsChanged(contexts)
577 }362 onReportError: Manager.reportError(message)
363 Component.onCompleted: Manager.contextsChanged(sim.connMan.contexts)
364 }
365
366 // We set the target to be ConnMan before we want to call 'ResetContexts' on
367 // ConnMan. When ConnMan powers down, the connManPoweredChanged handler is
368 // called and call 'ResetContexts'. This is because we can't make this call
369 // while ConnMan is 'Powered'. After the 'ResetContexts' call is done,
370 // the target is reset to null.
371 Connections {
372 id: restorePowered
373 target: null
374 ignoreUnknownSignals: true
375 onPoweredChanged: Manager.connManPoweredChanged(powered)
578 }376 }
579}377}
580378
=== added file 'plugins/cellular/apn_editor.js'
--- plugins/cellular/apn_editor.js 1970-01-01 00:00:00 +0000
+++ plugins/cellular/apn_editor.js 2015-07-27 14:21:15 +0000
@@ -0,0 +1,192 @@
1/*
2 * This file is part of system-settings
3 *
4 * Copyright (C) 2015 Canonical Ltd.
5 *
6 * Contact: Jonas G. Drange <jonas.drange@canonical.com>
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 *
20 * This is a collection of functions to help the custom apn editor.
21 */
22
23/**
24 * Updates the given OfonoContextConnection by using values from the editor.
25 *
26 * @param {OfonoContextConnection} qml to be updated
27*/
28function updateContextQML (ctx) {
29 var toType = indexToType(typeSel.selectedIndex);
30 toType = toType === 'internet+mms' ? 'internet' : toType;
31 ctx.disconnect();
32 ctx.name = name.text;
33 ctx.type = toType;
34 ctx.accessPointName = accessPointName.text;
35 ctx.username = username.text;
36 ctx.password = password.text;
37 ctx.messageCenter = messageCenter.visible ? messageCenter.text : '';
38 ctx.messageProxy = messageProxy.visible ? messageProxy.text + (port.text ? ':' + port.text : '') : '';
39}
40
41/**
42 * Populates editor with values from a OfonoContextConnection.
43 *
44 * @param {OfonoContextConnection} qml to use as reference
45*/
46function populate (ctx) {
47 name.text = ctx.name;
48 accessPointName.text = ctx.accessPointName;
49 username.text = ctx.username;
50 password.text = ctx.password;
51 messageCenter.text = ctx.messageCenter;
52 messageProxy.text = ctx.messageProxy;
53
54 if (ctx.isCombined) {
55 typeSel.selectedIndex = typeToIndex('internet+mms');
56 } else {
57 typeSel.selectedIndex = typeToIndex(ctx.type);
58 }
59}
60
61/**
62 * Handler for when a user saves a context.
63*/
64function saving () {
65 var model;
66 var type;
67 root.saving();
68
69 // Edit or new? Create a context if it does not exist.
70 if (contextQML) {
71 updateContextQML(contextQML);
72 root.saved();
73 } else {
74 type = indexToType();
75 if (type === 'internet+mms') type = 'internet';
76 manager.createContext(type);
77 }
78}
79
80/**
81 * Handler for new contexts.
82 *
83 * @param {OfonoContextConnection} new context
84*/
85function newContext (context) {
86 // Start a timer that will update the context.
87 // Ofono and libqofono seems to be very unreliable
88 // when it comes to how a context is created,
89 // so we just wait a couple of seconds until we're
90 // sure the context exists and can be edited.
91 updateContext.ctx = context;
92 updateContext.start();
93}
94
95/**
96 * Checks whether or not a link has a http protocol prefix.
97 *
98 * @param {String} link to check
99*/
100function hasProtocol (link) {
101 return link.search(/^http[s]?\:\/\//) == -1;
102}
103
104/**
105 * Prepend http:// to a link if there isn't one.
106 *
107 * @param {String} link to add http:// to
108 * @return {String} changed link
109*/
110function setHttp (link) {
111 if (hasProtocol(link)) {
112 link = 'http://' + link;
113 }
114 return link;
115}
116
117/**
118 * Asks whether or not the values in the editor is valid or not.
119 *
120 * @return {Boolean} whether or not the editor is valid
121*/
122function isValid () {
123 if (isMms || isCombo) {
124 return name.text &&
125 accessPointName.text &&
126 messageCenter.text;
127 } else {
128 return name.text &&
129 accessPointName.text;
130 }
131}
132
133/**
134 * Given a type, this asks what index of the type selector
135 * it corresponds to.
136 *
137 * @param {String} type to check
138 * @return {Number} of index
139*/
140function typeToIndex (type) {
141 if (type === 'internet+mms') return 0;
142 if (type === 'internet') return 1;
143 if (type === 'mms') return 2;
144 if (type === 'ia') return 3;
145}
146
147/**
148 * Given an index, we ask what type this index corresponds to.
149 *
150 * @param {Number} [optional] index to check
151 * @return {String} type it corresponds to
152*/
153function indexToType (index) {
154 if (typeof index === 'undefined') {
155 index = typeSel.selectedIndex;
156 }
157 if (index === 0) return 'internet+mms';
158 if (index === 1) return 'internet';
159 if (index === 2) return 'mms';
160 if (index === 3) return 'ia';
161}
162
163function ready () {
164 _edgeReady = true;
165}
166
167function makeMeVisible(item) {
168 if (!_edgeReady || !item) {
169 return;
170 }
171
172 root.activeItem = item;
173
174 var position = scrollArea.contentItem.mapFromItem(item, 0, root.activeItem.y);
175
176 // check if the item is already visible
177 var bottomY = scrollArea.contentY + scrollArea.height;
178 var itemBottom = position.y + item.height; // extra margin
179 if (position.y >= scrollArea.contentY && itemBottom <= bottomY) {
180 return;
181 }
182
183 // if it is not, try to scroll and make it visible
184 var targetY = itemBottom - scrollArea.height;
185 if (targetY >= 0 && position.y) {
186 scrollArea.contentY = targetY;
187 } else if (position.y < scrollArea.contentY) {
188 // if it is hidden at the top, also show it
189 scrollArea.contentY = position.y;
190 }
191 scrollArea.returnToBounds();
192}
0193
=== renamed file 'plugins/cellular/apn.js' => 'plugins/cellular/apn_manager.js'
--- plugins/cellular/apn.js 2015-03-26 21:57:53 +0000
+++ plugins/cellular/apn_manager.js 2015-07-27 14:21:15 +0000
@@ -1,61 +1,589 @@
1var _CUSTOM_INTERNET_CONTEXT_NAME = '___ubuntu_custom_apn_internet';1/*
2var _CUSTOM_MMS_CONTEXT_NAME = '___ubuntu_custom_apn_mms';2 * This file is part of system-settings
33 *
4/*4 * Copyright (C) 2015 Canonical Ltd.
5Updates a context with new values.5 *
66 * Contact: Jonas G. Drange <jonas.drange@canonical.com>
7@param {QOfonoConnectionContext} context7 *
8@param {Object} values dict with new values8 * This program is free software: you can redistribute it and/or modify it
9*/9 * under the terms of the GNU General Public License version 3, as published
10function updateContext (context, values) {10 * by the Free Software Foundation.
11 var messageProxy;11 *
12 console.warn('updateContext', values.accessPointName, values.messageCenter,12 * This program is distributed in the hope that it will be useful, but
13 values.messageProxy, values.port, values.useraccessPointName,13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 values.password, values.type);14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 if (typeof values.accessPointName !== 'undefined') {15 * PURPOSE. See the GNU General Public License for more details.
16 context.accessPointName = values.accessPointName;16 *
17 }17 * You should have received a copy of the GNU General Public License along
18 if (typeof values.messageCenter !== 'undefined') {18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 context.messageCenter = values.messageCenter;19 *
20 }20 * This is a collection of functions to help dynamic creation and deletion
21 if (typeof values.messageProxy !== 'undefined') {21 * of ofono contexts.
22 messageProxy = values.messageProxy;22 */
23 if (messageProxy !== '') {23
24 messageProxy = messageProxy + ':' + values.port;24// Map of path to QOfonoContextConnection objects
25 }25var _pathToQml = {};
26 context.messageProxy = messageProxy;26var _totalContext = 0;
27 }27var _validContexts = 0;
28 if (typeof values.username !== 'undefined') {28
29 context.username = values.username;29/**
30 }30 * Get the list model corresponding to a given type.
31 if (typeof values.password !== 'undefined') {31 *
32 context.password = values.password;32 * @throws {Error} if the type was not mms|internet|ia
33 }33 * @param {String} type of model to get
34 if (typeof values.type !== 'undefined') {34 * @return {ListModel} model that matches type
35 context.type = values.type;35*/
36 }36function getModelFromType (type) {
3737 var model;
38 if (context.type === 'internet') {38 switch (type) {
39 context.name = CUSTOM_INTERNET_CONTEXT_NAME();39 case 'mms':
40 } else if (context.type === 'mms') {40 model = mmsContexts;
41 context.name = CUSTOM_MMS_CONTEXT_NAME();41 break;
42 }42 case 'internet':
43}43 case 'internet+mms':
4444 model = internetContexts;
45/*45 break;
46Exposes the custom internet context name to the world.46 case 'ia':
4747 model = iaContexts;
48@return {String} custom internet context name48 break;
49*/49 default:
50function CUSTOM_INTERNET_CONTEXT_NAME () {50 throw new Error('Unknown context type ' + type);
51 return _CUSTOM_INTERNET_CONTEXT_NAME;51 }
52}52 return model;
5353}
54/*54
55Exposes the custom mms context name to the world.55/**
5656 * Get QML for a context path.
57@return {String} custom mms context name57 *
58*/58 * @param {String} path of context
59function CUSTOM_MMS_CONTEXT_NAME () {59 * @return {QOfonoContextConnection|null} qml from path or null if none found
60 return _CUSTOM_MMS_CONTEXT_NAME;60*/
61function getContextQML (path) {
62 if (!_pathToQml.hasOwnProperty(path)) {
63 return null;
64 } else {
65 return _pathToQml[path];
66 }
67}
68
69/**
70 * Given an array of paths, it will create and associate
71 * an QOfonoContextConnection QML object for each new path.
72 *
73 * It will also delete any QML that is not in given list of paths.
74 *
75 * @param {Array} paths, array of operator paths
76 */
77function updateQML (paths) {
78 _garbageCollect(paths);
79 _createQml(paths);
80}
81
82/**
83 * Destroys QML and makes sure to remove from the
84 * appropriate model.
85 *
86 * @param {String} path of object to delete
87 * @return {Boolean} deletion successful
88 */
89function deleteQML (path) {
90 var ctx;
91 var i;
92 if (!_pathToQml.hasOwnProperty(path)) {
93 return false;
94 } else {
95 ctx = _pathToQml[path];
96
97 [mmsContexts, internetContexts, iaContexts].forEach(function (model) {
98 for (i = 0; i < model.count; i++) {
99 if (ctx.contextPath == model.get(i).path) {
100 model.remove(i);
101 break;
102 }
103 }
104 });
105
106 _pathToQml[path].destroy();
107 delete _pathToQml[path];
108 return true;
109 }
110}
111
112/**
113 * Removes QML that no longer exists in list of paths.
114 *
115 * @param {Array:String} paths we use as reference.
116 */
117function _garbageCollect (paths) {
118 var path;
119 for (path in _pathToQml) {
120 if (_pathToQml.hasOwnProperty(path)) {
121 if (paths.indexOf(path) === -1) {
122 deleteQML(path);
123 _totalContext--;
124 }
125 }
126 }
127}
128
129/**
130 * Creates QML for list in paths.
131 *
132 * @param {Array:String} list of paths
133 * @param {String} path to the modem
134 */
135function _createQml (paths) {
136 var ctx;
137 paths.forEach(function (path, i) {
138 if (!_pathToQml.hasOwnProperty(path)) {
139
140 ctx = createContextQml(path);
141
142 // Some contexts have a name, others do not. Normalize this.
143 if (!ctx.name) {
144 ctx.nameChanged.connect(contextNameChanged.bind(ctx));
145 } else {
146 contextNameChanged.bind(ctx)(ctx.name);
147 }
148
149 // Some context come with a type, others not. Normalize this.
150 if (!ctx.type) {
151 ctx.typeChanged.connect(contextTypeChanged.bind(ctx));
152 } else {
153 addContextToModel(ctx);
154 }
155
156 ctx.validChanged.connect(contextValidChanged.bind(ctx));
157 ctx.activeChanged.connect(contextActiveChanged.bind(ctx));
158
159 _pathToQml[path] = ctx;
160 _totalContext++;
161 }
162 });
163}
164
165/**
166 * Creates a OfonoContextConnection qml object from a given path.
167 * Since the components are all local, this will always return an object.
168 *
169 * @param {String} path of context
170 * @return {OfonoContextConnection} qml that was created
171*/
172function createContextQml (path) {
173 if (!_pathToQml.hasOwnProperty(path)) {
174 return contextComponent.createObject(root, {
175 'contextPath': path,
176 'modemPath': sim.path
177 });
178 } else {
179 return _pathToQml[path];
180 }
181}
182
183/**
184 * Creates a context of a certain type.
185 *
186 * @param {String} type of context to be created.
187*/
188function createContext (type) {
189 sim.connMan.addContext(type);
190}
191
192/**
193 * Removes a context. We don't remove any QML until we receive signal from
194 * ofono that the context was removed, but we disconnect it if active.
195 *
196 * @param {String} path of context to be removed
197*/
198function removeContext (path) {
199 var ctx = getContextQML(path);
200
201 if (ctx && ctx.active) {
202 ctx.disconnect();
203 }
204
205 sim.connMan.removeContext(path);
206}
207
208/**
209 * Adds a context to the appropriate model. If the context to be added is found
210 * in another model, which will happen if the user changes type of the context,
211 * we remove it from the old model and add it to the new.
212 *
213 * @param {OfonoContextConnection} ctx to be added
214 * @param {String} [optional] type of context
215*/
216function addContextToModel (ctx, type) {
217 var data = {
218 path: ctx.contextPath,
219 qml: ctx
220 };
221 var model;
222 var oldModel;
223 var haveContext;
224
225 // We will move a model if it already exist.
226 [mmsContexts, internetContexts, iaContexts].forEach(function (m) {
227 var i;
228 for (i = 0; i < m.count && !haveContext; i++) {
229 if (ctx.contextPath == m.get(i).path) {
230 haveContext = m.get(i);
231 oldModel = m;
232 break;
233 }
234 }
235 });
236
237 if (typeof type === 'undefined') {
238 type = ctx.type;
239 }
240
241 if (haveContext && oldModel) {
242 oldModel.remove(haveContext);
243 }
244
245 model = getModelFromType(type);
246 model.append(data);
247}
248
249/**
250 * Removes a context from the appropriate model.
251 *
252 * @param {OfonoContextConnection} ctx to be removed
253 * @param {String} [optional] type of context
254*/
255function removeContextFromModel (ctx, type) {
256 var model = getModelFromType(type);
257 var i;
258
259 if (typeof type === 'undefined') {
260 type = ctx.type;
261 }
262
263 for (i = 0; i < model.count; i++) {
264 if (model.get(i).path === ctx.contextPath) {
265 model.remove(i);
266 return;
267 }
268 }
269}
270
271/**
272 * Handler for removed contexts.
273 *
274 * @param {String} path that was removed
275*/
276function contextRemoved (path) {
277 var paths = sim.connMan.contexts.slice(0);
278 var updatedPaths = paths.filter(function (val) {
279 return val !== path;
280 });
281 _garbageCollect(paths);
282}
283
284/**
285 * Handler for when a type has been determined. If a contex changes type,
286 * we need to move it to the correct model.
287 * Note that “this' refers to the context on which type changed.
288 *
289 * @param {String} type
290 */
291function contextTypeChanged (type) {
292 addContextToModel(this, type);
293}
294
295/**
296 * Handler for when validity of context changes.
297 * Note that “this' refers to the context on which valid changed.
298 *
299 * @param {Boolean} valid
300 */
301function contextValidChanged (valid) {
302 if (valid) {
303 _validContexts++;
304 } else {
305 _validContexts--;
306 }
307
308 if (_validContexts === _totalContext) {
309 root.ready();
310 }
311}
312
313/**
314 * Handler for when active changes.
315 * Note that “this' refers to the context on which active changed.
316 *
317 * @param {Boolean} active
318 */
319function contextActiveChanged (active) {
320 if (active) {
321 checkPreferred();
322 }
323}
324
325/**
326 * This is code that is supposed to identify new contexts that user creates.
327 * If we think the context is new, and the editor page is open, we notify it.
328 *
329 * Note that “this' refers to the context on which name changed.
330 *
331 * @param {String} name's new value
332*/
333function contextNameChanged (name) {
334 switch (name) {
335 case 'Internet':
336 case 'IA':
337 case 'MMS':
338 if (editor) {
339 editor.newContext(this);
340 }
341 break;
342 }
343 this.nameChanged.disconnect(contextNameChanged);
344}
345
346/**
347 * Handler for added contexts.
348 *
349 * @param {String} path which was added
350 */
351function contextAdded (path) {
352 _createQml([path]);
353}
354
355/**
356 * Handler for when contexts change.
357 *
358 * @param {Array:String} paths after change
359 */
360function contextsChanged (paths) {
361 updateQML(paths);
362 checkPreferred();
363}
364
365/**
366 * Handler for when errors are reported from ofono.
367 *
368 * @param {String} message from libqofono
369 */
370function reportError (message) {
371 console.error(message);
372}
373
374/**
375 * Set Preferred on a context. A side effect of this, if value is true, is
376 * that all other contexts of the same type will get de-preferred. If an
377 * MMS context is preferred, all other MMS contexts are de-preferred.
378 *
379 * There is also a case where if you prefer an MMS context when there already
380 * is a preferred Internet+MMS (combined) context. In this case we prompt the
381 * user what to do.
382 *
383 * Note: If triggered by a CheckBox, the “this” argument will be the CheckBox.
384 *
385 * @param {OfonoContextConnection} context to prefer
386 * @param {Boolean} new preferred value
387 * @param {Boolean} whether or not to force this action, even though it may
388 * decrease the level of connectivity of the user.
389*/
390function setPreferred (context, value, force) {
391 var conflictingContexts = getConflictingContexts(context);
392 var mmsPreferralCausesCombinedDePreferral;
393 var internetPreferralCausesCombinedDePreferral;
394
395 // The context is about to be de-preferred.
396 if (!value) {
397 if (force) {
398 context.preferred = false;
399 } else {
400 PopupUtils.open(disableContextWarning, root, {
401 context: context
402 });
403 this.checked = true;
404 }
405 return;
406 }
407
408 // If user is preferring standalone Internet or standalone MMS context,
409 // over a combined context, we will give a warning, if not forced.
410 conflictingContexts.forEach(function (ctxC) {
411 if (ctxC.isCombined) {
412 if (context.type === 'mms') {
413 mmsPreferralCausesCombinedDePreferral = ctxC;
414 }
415
416 if (context.type === 'internet') {
417 internetPreferralCausesCombinedDePreferral = ctxC;
418 }
419 }
420 });
421
422 if (mmsPreferralCausesCombinedDePreferral && !force) {
423 PopupUtils.open(disablesInternetWarning, root, {
424 combined: mmsPreferralCausesCombinedDePreferral,
425 mms: context
426 });
427 this.checked = false;
428 return;
429 } else if (internetPreferralCausesCombinedDePreferral && !force) {
430 PopupUtils.open(disablesMMSWarning, root, {
431 combined: internetPreferralCausesCombinedDePreferral,
432 internet: context
433 });
434 this.checked = false;
435 return;
436 }
437
438 conflictingContexts.forEach(function (ctx) {
439 ctx.preferred = false;
440 });
441
442 context.preferred = true;
443}
444
445/**
446 * Reset apn configuration.
447 */
448function reset () {
449 // If cellular data is on, we need to turn it off. The reset itself,
450 // as well as turning cellular data back on, is done by the use of a
451 // Connection component and connManPoweredChanged.
452 if (sim.connMan.powered) {
453 restorePowered.target = sim.connMan;
454 sim.connMan.powered = false;
455 } else {
456 connManPoweredChanged(sim.connMan.powered);
457 }
458}
459
460
461/**
462 * Handler for when powered changed. This handler is attached to a signal by
463 * a Connections component in PageChooseApn.qml.
464 */
465function connManPoweredChanged (powered) {
466 if (!powered) {
467
468 // We want to fire the ready signal again, once we've reset, but
469 // the reset contexts won't necessarily fire 'validChanged' signals,
470 // so we manually set valid contexts to 0.
471 _validContexts = 0;
472 root.ready.connect(ready);
473
474 sim.connMan.resetContexts();
475
476 // If restorePowered had a target, we know to turn cellular
477 // data back on.
478 if (restorePowered.target) {
479 sim.connMan.powered = true;
480 }
481 }
482 restorePowered.target = null;
483}
484
485/**
486 * Checks if there are preferred contexts. If there are none,
487 * we prefer the active one.
488 */
489function checkPreferred () {
490 var models = [internetContexts, iaContexts, mmsContexts];
491
492 models.forEach(function (model) {
493 var i;
494 var ctx;
495 var activeCtx;
496
497 // Find active contexts in model.
498 for (i = 0; i < model.count; i++) {
499 ctx = model.get(i).qml;
500
501 if (ctx.active) {
502 activeCtx = ctx;
503 }
504 }
505
506 if (activeCtx && getConflictingContexts(activeCtx).length === 0) {
507 activeCtx.preferred = true;
508 }
509 });
510}
511
512/**
513 * Gives a list of conflicting contexts, i.e. contexts that are preferred
514 * and will create problems† if preferred at the same time as the given
515 * context.
516 *
517 * † Multiple preferred contexts of the same type will cause undefined
518 * Nuntium and NetworkManager behaviour.
519 *
520 * @param {OfonoContextConnection|String} context to be preferred and to check
521 conflicts against, or type as string
522 * @return {Array:OfonoContextConnection} list of OfonoContextConnection that
523 * are in conflict, excluding itself
524 *
525 */
526function getConflictingContexts (context) {
527 var type;
528 var conflicts = [];
529 var i;
530 var ctxI;
531 var typeModel;
532
533 if (typeof context === 'string') {
534 type = context;
535 } else {
536 type = context.isCombined ? 'internet+mms' : context.type;
537 }
538
539 switch (type) {
540 // A combined context will conflict with internet contexts and MMS
541 // contexts.
542 case 'internet+mms':
543 [internetContexts, mmsContexts].forEach(function (model) {
544 var i;
545 for (i = 0; i < model.count; i++) {
546 ctxI = model.get(i).qml;
547 if (ctxI.preferred) {
548 conflicts.push(ctxI);
549 }
550 }
551 });
552 break;
553 // An MMS context will conflict with other MMS contexts, as well as
554 // combined contexts.
555 case 'mms':
556 for (i = 0; i < mmsContexts.count; i++) {
557 ctxI = mmsContexts.get(i).qml;
558 if (ctxI.preferred) {
559 conflicts.push(ctxI);
560 }
561 }
562 for (i = 0; i < internetContexts.count; i++) {
563 ctxI = internetContexts.get(i).qml;
564 if (ctxI.isCombined && ctxI.preferred) {
565 conflicts.push(ctxI);
566 }
567 }
568 break;
569 case 'internet':
570 case 'ia':
571 typeModel = getModelFromType(type);
572 for (i = 0; i < typeModel.count; i++) {
573 ctxI = typeModel.get(i).qml;
574 if (ctxI.preferred) {
575 conflicts.push(ctxI);
576 }
577 }
578 break;
579 default:
580 throw new Error('Can\'t resolve conflicts for type' + type);
581 }
582
583 return conflicts;
584}
585
586function ready () {
587 checkPreferred();
588 root.ready.disconnect(ready);
61}589}
62590
=== removed file 'plugins/cellular/ofonoactivator.cpp'
--- plugins/cellular/ofonoactivator.cpp 2014-10-09 10:50:50 +0000
+++ plugins/cellular/ofonoactivator.cpp 1970-01-01 00:00:00 +0000
@@ -1,132 +0,0 @@
1/*
2 * Copyright (C) 2014 Canonical, Ltd.
3 *
4 * Authors:
5 * Jussi Pakkanen <jussi.pakkanen@canonical.com>
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License version 3, as published
9 * by the Free Software Foundation.
10 *
11 * This library is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
14 * details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include "ofonoactivator.h"
21
22#include"nm_manager_proxy.h"
23#include"nm_settings_proxy.h"
24#include"nm_settings_connection_proxy.h"
25#include<QCoreApplication>
26
27typedef QMap<QString,QVariantMap> Vardict;
28Q_DECLARE_METATYPE(Vardict)
29
30namespace {
31
32QString nmService("org.freedesktop.NetworkManager");
33QString nmSettingsPath("/org/freedesktop/NetworkManager/Settings");
34QString nmPath("/org/freedesktop/NetworkManager");
35
36QDBusObjectPath detectConnection(const QString &ofonoContext, const QString imsi) {
37 auto ofonoContextBase = ofonoContext.split('/').back();
38 auto target = "/" + imsi + "/" + ofonoContextBase;
39
40 OrgFreedesktopNetworkManagerSettingsInterface settings(nmService, nmSettingsPath,
41 QDBusConnection::systemBus());
42 auto reply = settings.ListConnections();
43 reply.waitForFinished();
44 if(!reply.isValid()) {
45 qWarning() << "Error getting connection list: " << reply.error().message() << "\n";
46 }
47 auto connections = reply.value(); // Empty list if failed.
48
49 for(const auto &c : connections) {
50 OrgFreedesktopNetworkManagerSettingsConnectionInterface connProxy(nmService,
51 c.path(), QDBusConnection::systemBus());
52 auto reply2 = connProxy.GetSettings();
53 reply2.waitForFinished();
54 if(!reply2.isValid()) {
55 qWarning() << "Error getting property: " << reply2.error().message() << "\n";
56 continue;
57 }
58 auto settings = reply2.value();
59 auto id = settings["connection"]["id"].toString();
60 if(id == target) {
61 return c;
62 }
63 }
64 return QDBusObjectPath("");
65}
66
67QDBusObjectPath detectDevice(const QString &modemPath) {
68 OrgFreedesktopNetworkManagerInterface nm(nmService, nmPath, QDBusConnection::systemBus());
69 auto reply = nm.GetDevices();
70 reply.waitForFinished();
71 auto devices = reply.value();
72
73 for(const auto &device : devices) {
74 QDBusInterface iface(nmService, device.path(), "org.freedesktop.DBus.Properties",
75 QDBusConnection::systemBus());
76 QDBusReply<QDBusVariant> ifaceReply = iface.call("Get",
77 "org.freedesktop.NetworkManager.Device", "Interface");
78 if(!ifaceReply.isValid()) {
79 qWarning() << "Error getting property: " << ifaceReply.error().message() << "\n";
80 continue;
81 }
82 auto devIface = ifaceReply.value().variant().toString();
83 if(devIface == modemPath) {
84 return device;
85 }
86 }
87 return QDBusObjectPath("");
88}
89}
90
91void activateOfono(QDBusObjectPath connection, QDBusObjectPath device)
92{
93 OrgFreedesktopNetworkManagerInterface nm(nmService, nmPath, QDBusConnection::systemBus());
94
95
96 /// @bug https://bugs.launchpad.net/ubuntu/+source/network-manager/+bug/1378102
97 /// This is a big and fat workaround for the above bug
98 // this is probably racy as well as we are not tracking the ActiveConnection to know
99 // when the Device has actually disconnected.
100 QDBusInterface dev_iface("org.freedesktop.NetworkManager",
101 device.path(),
102 "org.freedesktop.NetworkManager.Device",
103 nm.connection());
104 dev_iface.call("Disconnect");
105
106
107 nm.ActivateConnection(connection, device, QDBusObjectPath("/"));
108}
109
110OfonoActivator::OfonoActivator(QObject *parent) : QObject(parent) {
111 static bool isRegistered = false;
112 if(!isRegistered) {
113 qDBusRegisterMetaType<Vardict>();
114 isRegistered = true;
115 }
116}
117
118Q_INVOKABLE bool OfonoActivator::activate(const QString ofonoContext, const QString imsi, const QString modemPath)
119{
120 auto dev = detectDevice(modemPath);
121 if(dev.path() == "") {
122 qWarning() << "Could not detect device object to use for Ofono activation.\n";
123 return false;
124 }
125 auto conn = detectConnection(ofonoContext, imsi);
126 if(conn.path() == "") {
127 qWarning() << "Could not detect connection object to use for Ofono activation.\n";
128 return false;
129 }
130 activateOfono(conn, dev);
131 return true;
132}
1330
=== removed file 'plugins/cellular/ofonoactivator.h'
--- plugins/cellular/ofonoactivator.h 2014-08-27 09:11:52 +0000
+++ plugins/cellular/ofonoactivator.h 1970-01-01 00:00:00 +0000
@@ -1,42 +0,0 @@
1/*
2 * Copyright (C) 2014 Canonical, Ltd.
3 *
4 * Authors:
5 * Jussi Pakkanen <jussi.pakkanen@canonical.com>
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License version 3, as published
9 * by the Free Software Foundation.
10 *
11 * This library is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
14 * details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#ifndef OFONO_ACTIVATOR
21#define OFONO_ACTIVATOR
22
23#include <QObject>
24
25/**
26 * For exposing ofono controls to qml.
27 */
28
29class OfonoActivator : public QObject {
30 Q_OBJECT
31
32public:
33 OfonoActivator(QObject *parent = nullptr);
34 ~OfonoActivator() {};
35
36 Q_INVOKABLE bool activate(const QString ofonoContext, const QString imsi, const QString modemPath);
37
38private:
39};
40
41
42#endif
430
=== modified file 'plugins/cellular/plugin.cpp'
--- plugins/cellular/plugin.cpp 2015-01-21 20:44:20 +0000
+++ plugins/cellular/plugin.cpp 2015-07-27 14:21:15 +0000
@@ -21,7 +21,6 @@
21#include <QtQml/QQmlContext>21#include <QtQml/QQmlContext>
22#include "connectivity.h"22#include "connectivity.h"
23#include "hotspotmanager.h"23#include "hotspotmanager.h"
24#include "ofonoactivator.h"
2524
26static QObject *connectivitySingeltonProvider(QQmlEngine *engine, QJSEngine *scriptEngine)25static QObject *connectivitySingeltonProvider(QQmlEngine *engine, QJSEngine *scriptEngine)
27{26{
@@ -37,7 +36,6 @@
37 Q_ASSERT(uri == QLatin1String("Ubuntu.SystemSettings.Cellular"));36 Q_ASSERT(uri == QLatin1String("Ubuntu.SystemSettings.Cellular"));
38 qmlRegisterSingletonType<Connectivity>(uri, 1, 0, "Connectivity", connectivitySingeltonProvider);37 qmlRegisterSingletonType<Connectivity>(uri, 1, 0, "Connectivity", connectivitySingeltonProvider);
39 qmlRegisterType<HotspotManager>(uri, 1, 0, "HotspotManager");38 qmlRegisterType<HotspotManager>(uri, 1, 0, "HotspotManager");
40 qmlRegisterType<OfonoActivator>(uri, 1, 0, "OfonoActivator");
41}39}
4240
43void BackendPlugin::initializeEngine(QQmlEngine *engine, const char *uri)41void BackendPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
4442
=== modified file 'tests/autopilot/ubuntu_system_settings/__init__.py'
--- tests/autopilot/ubuntu_system_settings/__init__.py 2015-07-22 12:54:52 +0000
+++ tests/autopilot/ubuntu_system_settings/__init__.py 2015-07-27 14:21:15 +0000
@@ -187,6 +187,12 @@
187 return False187 return False
188188
189189
190class LabelTextField(ubuntuuitoolkit.TextField):
191 """LabelTextField is a component local to the APN Editor in the cellular
192 plugin."""
193 pass
194
195
190class CellularPage(ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):196class CellularPage(ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
191197
192 """Autopilot helper for the Cellular page."""198 """Autopilot helper for the Cellular page."""
@@ -262,6 +268,24 @@
262 chooseCarrierPage.set_carrier(carrier)268 chooseCarrierPage.set_carrier(carrier)
263269
264 @autopilot.logging.log_action(logger.debug)270 @autopilot.logging.log_action(logger.debug)
271 def open_apn_editor(self, name, sim=None):
272 carrierApnPage = self._click_carrier_apn()
273 chooseApnPage = carrierApnPage.open_apn(sim)
274 return chooseApnPage.open(name)
275
276 @autopilot.logging.log_action(logger.debug)
277 def delete_apn(self, name, sim=None):
278 carrierApnPage = self._click_carrier_apn()
279 chooseApnPage = carrierApnPage.open_apn(sim)
280 return chooseApnPage.delete(name)
281
282 @autopilot.logging.log_action(logger.debug)
283 def prefer_apn(self, name, sim=None):
284 carrierApnPage = self._click_carrier_apn()
285 chooseApnPage = carrierApnPage.open_apn(sim)
286 return chooseApnPage.check(name)
287
288 @autopilot.logging.log_action(logger.debug)
265 def _click_carrier_apn(self):289 def _click_carrier_apn(self):
266 item = self.select_single(objectName='carrierApnEntry')290 item = self.select_single(objectName='carrierApnEntry')
267 self.pointing_device.click_object(item)291 self.pointing_device.click_object(item)
@@ -351,11 +375,22 @@
351 return self.get_root_instance().wait_select_single(375 return self.get_root_instance().wait_select_single(
352 objectName='chooseCarrierPage')376 objectName='chooseCarrierPage')
353377
378 @autopilot.logging.log_action(logger.debug)
379 def open_apn(self, sim):
380 return self._click_apn(sim)
381
382 @autopilot.logging.log_action(logger.debug)
383 def _click_apn(self, sim):
384 obj = self.select_single(
385 objectName='apn')
386 self.pointing_device.click_object(obj)
387 return self.get_root_instance().wait_select_single(
388 objectName='apnPage')
389
354390
355class PageCarriersAndApns(391class PageCarriersAndApns(
356 ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):392 ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
357 """Autopilot helper for carrier/apn entry page (multisim)."""393 """Autopilot helper for carrier/apn entry page (multisim)."""
358 """Autopilot helper for carrier/apn entry page (singlesim)."""
359 @autopilot.logging.log_action(logger.debug)394 @autopilot.logging.log_action(logger.debug)
360 def open_carrier(self, sim):395 def open_carrier(self, sim):
361 return self._click_carrier(sim)396 return self._click_carrier(sim)
@@ -378,6 +413,7 @@
378 item = self.select_single(text='Automatically')413 item = self.select_single(text='Automatically')
379 self.pointing_device.click_object(item)414 self.pointing_device.click_object(item)
380415
416 @autopilot.logging.log_action(logger.debug)
381 def set_carrier(self, carrier):417 def set_carrier(self, carrier):
382 # wait for animation, since page.animationRunning.wait_for(False)418 # wait for animation, since page.animationRunning.wait_for(False)
383 # does not work?419 # does not work?
@@ -396,6 +432,116 @@
396 self.pointing_device.click_object(item)432 self.pointing_device.click_object(item)
397433
398434
435class PageChooseApn(ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
436
437 """Autopilot helper for apn editor page"""
438
439 @autopilot.logging.log_action(logger.debug)
440 def open(self, name):
441 return self._open_editor(name)
442
443 @autopilot.logging.log_action(logger.debug)
444 def delete(self, name):
445 self._delete(name)
446
447 @autopilot.logging.log_action(logger.debug)
448 def _delete(self, name):
449 item = self.wait_select_single('Standard', objectName='edit_%s' % name)
450 item.swipe_to_delete()
451 item.confirm_removal()
452
453 @autopilot.logging.log_action(logger.debug)
454 def check(self, name):
455 self._check(name)
456
457 @autopilot.logging.log_action(logger.debug)
458 def _check(self, name):
459 item = self.wait_select_single(
460 'CheckBox', objectName='%s_preferred' % name
461 )
462 item.check()
463
464 @autopilot.logging.log_action(logger.debug)
465 def _open_editor(self, name):
466 if name:
467 item = self.select_single(objectName='edit_%s' % name)
468 self.pointing_device.click_object(item)
469 else:
470 main_view = self.get_root_instance().select_single(
471 objectName='systemSettingsMainView')
472 header = main_view.select_single('AppHeader')
473 header.click_action_button('newApn')
474 return self.get_root_instance().wait_select_single(
475 objectName='apnEditor')
476
477
478class PageApnEditor(ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
479
480 """Autopilot helper for apn editor page"""
481
482 flickable = None
483
484 def __init__(self, *args):
485 super().__init__(*args)
486 self.flickable = self.select_single(objectName='scrollArea')
487
488 @autopilot.logging.log_action(logger.debug)
489 def set_type(self, t):
490 selector = self.select_single(
491 'ItemSelector', objectName='typeSelector')
492 self.pointing_device.click_object(selector)
493 selector.currentlyExpanded.wait_for(True)
494 item = self.select_single(objectName='type_%s' % t)
495
496 # A bit dirty
497 while selector.currentlyExpanded:
498 self.pointing_device.click_object(item)
499
500 @autopilot.logging.log_action(logger.debug)
501 def set_name(self, new_name):
502 self._populate_field('name', new_name)
503
504 @autopilot.logging.log_action(logger.debug)
505 def set_access_point_name(self, new_name):
506 self._populate_field('accessPointName', new_name)
507
508 @autopilot.logging.log_action(logger.debug)
509 def set_message_center(self, new_message_center):
510 self._populate_field('messageCenter', new_message_center)
511
512 @autopilot.logging.log_action(logger.debug)
513 def set_message_proxy(self, new_message_proxy):
514 self._populate_field('messageProxy', new_message_proxy)
515
516 # Sleep for the duration of the timer that will copy any
517 # port into the port field
518 sleep(1.5)
519
520 @autopilot.logging.log_action(logger.debug)
521 def set_port(self, new_port):
522 self._populate_field('port', new_port)
523
524 @autopilot.logging.log_action(logger.debug)
525 def set_username(self, new_username):
526 self._populate_field('username', new_username)
527
528 @autopilot.logging.log_action(logger.debug)
529 def set_password(self, new_password):
530 self._populate_field('password', new_password)
531
532 def _populate_field(self, field, text):
533 f = self.select_single(LabelTextField, objectName=field)
534 self.flickable.swipe_child_into_view(f)
535 f.write(text)
536
537 @autopilot.logging.log_action(logger.debug)
538 def save(self):
539 main_view = self.get_root_instance().select_single(
540 objectName='systemSettingsMainView')
541 header = main_view.select_single('AppHeader')
542 header.click_action_button('saveApn')
543
544
399class SecurityPage(ubuntuuitoolkit.QQuickFlickable):545class SecurityPage(ubuntuuitoolkit.QQuickFlickable):
400546
401 """Autopilot helper for the Security page."""547 """Autopilot helper for the Security page."""
402548
=== modified file 'tests/autopilot/ubuntu_system_settings/tests/__init__.py'
--- tests/autopilot/ubuntu_system_settings/tests/__init__.py 2015-07-17 15:06:35 +0000
+++ tests/autopilot/ubuntu_system_settings/tests/__init__.py 2015-07-27 14:21:15 +0000
@@ -180,22 +180,6 @@
180 return 'ret = [(m, objects[m].GetAll("org.ofono.NetworkOperator")) ' \180 return 'ret = [(m, objects[m].GetAll("org.ofono.NetworkOperator")) ' \
181 'for m in objects if "%s/operator/" in m]' % name181 'for m in objects if "%s/operator/" in m]' % name
182182
183 def mock_connection_manager(self, modem):
184 modem.AddProperty(CONNMAN_IFACE, 'Powered', dbus.Boolean(1))
185 modem.AddProperty(CONNMAN_IFACE, 'RoamingAllowed', dbus.Boolean(0))
186 modem.AddMethods(
187 CONNMAN_IFACE,
188 [
189 (
190 'GetProperties', '', 'a{sv}',
191 'ret = self.GetAll("%s")' % CONNMAN_IFACE),
192 (
193 'SetProperty', 'sv', '',
194 'self.Set("IFACE", args[0], args[1]); '
195 'self.EmitSignal("IFACE", "PropertyChanged", "sv",\
196 [args[0], args[1]])'.replace("IFACE", CONNMAN_IFACE)),
197 ])
198
199 def mock_carriers(self, name):183 def mock_carriers(self, name):
200 self.dbusmock.AddObject(184 self.dbusmock.AddObject(
201 '/%s/operator/op2' % name,185 '/%s/operator/op2' % name,
@@ -284,7 +268,6 @@
284268
285 self.mock_carriers('ril_0')269 self.mock_carriers('ril_0')
286 self.mock_radio_settings(self.modem_0)270 self.mock_radio_settings(self.modem_0)
287 self.mock_connection_manager(self.modem_0)
288 self.mock_call_forwarding(self.modem_0)271 self.mock_call_forwarding(self.modem_0)
289 self.mock_call_settings(self.modem_0)272 self.mock_call_settings(self.modem_0)
290273
@@ -295,6 +278,9 @@
295 ('GetOperators', '', 'a(oa{sv})', self.get_all_operators('ril_0')),278 ('GetOperators', '', 'a(oa{sv})', self.get_all_operators('ril_0')),
296 ('Scan', '', 'a(oa{sv})', self.get_all_operators('ril_0')),279 ('Scan', '', 'a(oa{sv})', self.get_all_operators('ril_0')),
297 ])280 ])
281 self.modem_0.connMan = dbus.Interface(self.dbus_con.get_object(
282 'org.ofono', '/ril_0'),
283 'org.ofono.ConnectionManager')
298284
299 def add_sim2(self):285 def add_sim2(self):
300 '''Mock two modems/sims for the dual sim story'''286 '''Mock two modems/sims for the dual sim story'''
@@ -315,7 +301,6 @@
315 ])301 ])
316 self.mock_carriers(second_modem)302 self.mock_carriers(second_modem)
317 self.mock_radio_settings(self.modem_1, technologies=['gsm'])303 self.mock_radio_settings(self.modem_1, technologies=['gsm'])
318 self.mock_connection_manager(self.modem_1)
319 self.mock_call_forwarding(self.modem_1)304 self.mock_call_forwarding(self.modem_1)
320 self.mock_call_settings(self.modem_1)305 self.mock_call_settings(self.modem_1)
321306
@@ -324,6 +309,10 @@
324 'SubscriberNumbers', ['08123', '938762783']309 'SubscriberNumbers', ['08123', '938762783']
325 )310 )
326311
312 self.modem_1.connMan = dbus.Interface(self.dbus_con.get_object(
313 'org.ofono', '/' + second_modem),
314 'org.ofono.ConnectionManager')
315
327 @classmethod316 @classmethod
328 def setUpClass(cls):317 def setUpClass(cls):
329 cls.start_system_bus()318 cls.start_system_bus()
@@ -378,6 +367,16 @@
378 super(CellularBaseTestCase, self).setUp()367 super(CellularBaseTestCase, self).setUp()
379 self.cellular_page = self.main_view.go_to_cellular_page()368 self.cellular_page = self.main_view.go_to_cellular_page()
380369
370 def add_connection_context(self, modem, **kwargs):
371 iface = 'org.ofono.ConnectionContext'
372 path = modem.connMan.AddContext(kwargs.get('Type', 'internet'))
373 context = dbus.Interface(self.dbus_con.get_object(
374 'org.ofono', path),
375 iface)
376
377 for key, value in kwargs.items():
378 context.SetProperty(key, value)
379
381380
382class BluetoothBaseTestCase(UbuntuSystemSettingsTestCase):381class BluetoothBaseTestCase(UbuntuSystemSettingsTestCase):
383382
384383
=== modified file 'tests/autopilot/ubuntu_system_settings/tests/ofono.py'
--- tests/autopilot/ubuntu_system_settings/tests/ofono.py 2015-07-22 12:54:57 +0000
+++ tests/autopilot/ubuntu_system_settings/tests/ofono.py 2015-07-27 14:21:15 +0000
@@ -88,6 +88,7 @@
88 obj.name = name88 obj.name = name
89 obj.sim_pin = "2468"89 obj.sim_pin = "2468"
90 add_simmanager_api(obj)90 add_simmanager_api(obj)
91 add_connectionmanager_api(obj)
91 add_voice_call_api(obj)92 add_voice_call_api(obj)
92 add_netreg_api(obj)93 add_netreg_api(obj)
93 self.modems.append(path)94 self.modems.append(path)
@@ -127,6 +128,104 @@
127 ])128 ])
128129
129130
131def add_connectionmanager_api(mock):
132 '''Add org.ofono.ConnectionManager API to a mock'''
133
134 iface = 'org.ofono.ConnectionManager'
135 mock.contexts = []
136 mock.AddProperties(iface, {
137 'Attached': _parameters.get('Attached', True),
138 'Bearer': _parameters.get('Bearer', 'gprs'),
139 'RoamingAllowed': _parameters.get('RoamingAllowed', False),
140 'Powered': _parameters.get('ConnectionPowered', True),
141 })
142 mock.AddMethods(iface, [
143 ('GetProperties', '', 'a{sv}', 'ret = self.GetAll("%s")' % iface),
144 ('SetProperty', 'sv', '', 'self.Set("%(i)s", args[0], args[1]); '
145 'self.EmitSignal("%(i)s", "PropertyChanged", "sv", ['
146 'args[0], args[1]])' % {'i': iface}),
147 ('AddContext', 's', 'o', 'ret = self.AddConnectionContext(args[0])'),
148 ('RemoveContext', 'o', '', 'self.RemoveConnectionContext(args[0])'),
149 ('DeactivateAll', '', '', ''),
150 ('GetContexts', '', 'a(oa{sv})', 'ret = self.GetConnectionContexts()'),
151 ])
152
153 interfaces = mock.GetProperties()['Interfaces']
154 interfaces.append(iface)
155 mock.SetProperty('Interfaces', interfaces)
156
157
158@dbus.service.method('org.ofono.ConnectionManager',
159 in_signature='', out_signature='a(oa{sv})')
160def GetConnectionContexts(self):
161 contexts = dbus.Array([], signature='a(oa{sv})')
162 for ctx in self.contexts:
163 contexts.append(dbus.Struct(
164 (ctx.__dbus_object_path__, ctx.GetProperties())))
165 return contexts
166
167
168@dbus.service.method('org.ofono.ConnectionManager',
169 in_signature='s', out_signature='o')
170def AddConnectionContext(self, type):
171 name = 'context%s' % str(len(self.contexts))
172 path = '%s/%s' % (self.__dbus_object_path__, name)
173 iface = 'org.ofono.ConnectionContext'
174
175 # We give the context a name, just like ofono does.
176 # See https://github.com/rilmodem/ofono/blob/master/src/gprs.c#L148
177 ofono_default_accesspointname = {
178 'internet': 'Internet',
179 'mms': 'MMS',
180 'ai': 'AI',
181 'ims': 'IMS',
182 'wap': 'WAP'
183 }
184
185 self.AddObject(
186 path,
187 iface,
188 {
189 'Active': False,
190 'AccessPointName': '',
191 'Type': type,
192 'Username': '',
193 'Password': '',
194 'Protocol': 'ip',
195 'Name': ofono_default_accesspointname[type],
196 'Preferred': False,
197 'Settings': dbus.Dictionary({}, signature='sv'),
198 'IPv6.Settings': dbus.Dictionary({}, signature='sv'),
199 'MessageProxy': '',
200 'MessageCenter': '',
201 },
202 [
203 ('GetProperties', '', 'a{sv}',
204 'ret = self.GetAll("org.ofono.ConnectionContext")'),
205 (
206 'SetProperty', 'sv', '',
207 'self.Set("%s", args[0], args[1]); '
208 'self.EmitSignal("%s", "PropertyChanged",'
209 '"sv", [args[0], args[1]])' % (iface, iface)),
210 ])
211 ctx_obj = dbusmock.get_object(path)
212 self.contexts.append(ctx_obj)
213 self.EmitSignal('org.ofono.ConnectionManager',
214 'ContextAdded', 'oa{sv}', [path, ctx_obj.GetProperties()])
215
216 return path
217
218
219@dbus.service.method('org.ofono.ConnectionManager',
220 in_signature='o', out_signature='')
221def RemoveConnectionContext(self, path):
222 ctx_obj = dbusmock.get_object(path)
223 self.contexts.remove(ctx_obj)
224 self.RemoveObject(path)
225 self.EmitSignal('org.ofono.ConnectionManager',
226 'ContextRemoved', 'o', [path])
227
228
130@dbus.service.method('org.ofono.SimManager',229@dbus.service.method('org.ofono.SimManager',
131 in_signature='ss', out_signature='')230 in_signature='ss', out_signature='')
132def LockPin(self, pin_type, pin):231def LockPin(self, pin_type, pin):
133232
=== modified file 'tests/autopilot/ubuntu_system_settings/tests/test_cellular.py'
--- tests/autopilot/ubuntu_system_settings/tests/test_cellular.py 2014-10-29 15:51:17 +0000
+++ tests/autopilot/ubuntu_system_settings/tests/test_cellular.py 2015-07-27 14:21:15 +0000
@@ -288,3 +288,144 @@
288 lambda:288 lambda:
289 gsettings.get_value('default-sim-for-messages').get_string(),289 gsettings.get_value('default-sim-for-messages').get_string(),
290 Eventually(Equals('/ril_1')))290 Eventually(Equals('/ril_1')))
291
292
293class ApnTestCase(CellularBaseTestCase):
294
295 def test_remove_apn(self):
296 self.add_connection_context(self.modem_0, Type='mms', Name='Failed')
297 contexts = self.modem_0.connMan.GetContexts()
298
299 # Assert there's a Failed mms context
300 self.assertEqual(1, len(contexts))
301 self.assertEqual('/ril_0/context0', contexts[0][0])
302 self.assertEqual('Failed', contexts[0][1]['Name'])
303
304 # We delete the failed context
305 self.cellular_page.delete_apn('Failed')
306
307 def test_create_internet_apn(self):
308 editor = self.cellular_page.open_apn_editor(None)
309 editor.set_type('internet')
310 editor.set_name('Ubuntu')
311 editor.set_access_point_name('ubuntu.ap')
312 editor.set_username('user')
313 editor.set_password('pass')
314 editor.save()
315
316 # Wait for our new context to appear.
317 self.assertThat(
318 lambda: self.modem_0.connMan.GetContexts()[0][0],
319 Eventually(Equals('/ril_0/context0'))
320 )
321 # Wait for our context to be renamed from the default ofono name.
322 self.assertThat(
323 lambda: self.modem_0.connMan.GetContexts()[0][1]['Name'],
324 Eventually(Equals('Ubuntu'))
325 )
326 contexts = self.modem_0.connMan.GetContexts()
327 new_ctx = contexts[0][1]
328 self.assertEqual(1, len(contexts))
329 self.assertEqual('internet', new_ctx['Type'])
330 self.assertEqual('user', new_ctx['Username'])
331 self.assertEqual('pass', new_ctx['Password'])
332
333 def test_create_mms_apn(self):
334 editor = self.cellular_page.open_apn_editor(None)
335 editor.set_type('mms')
336 editor.set_name('Ubuntu')
337 editor.set_access_point_name('ubuntu.ap')
338 editor.set_message_center('ubuntu.com')
339 editor.set_message_proxy('ubuntu:8080')
340 editor.set_username('user')
341 editor.set_password('pass')
342 editor.save()
343
344 # Wait for our new context to appear.
345 self.assertThat(
346 lambda: self.modem_0.connMan.GetContexts()[0][0],
347 Eventually(Equals('/ril_0/context0'))
348 )
349 # Wait for our context to be renamed from the default ofono name.
350 self.assertThat(
351 lambda: self.modem_0.connMan.GetContexts()[0][1]['Name'],
352 Eventually(Equals('Ubuntu'))
353 )
354 contexts = self.modem_0.connMan.GetContexts()
355 new_ctx = contexts[0][1]
356 self.assertEqual(1, len(contexts))
357 self.assertEqual('mms', new_ctx['Type'])
358 self.assertEqual('ubuntu.ap', new_ctx['AccessPointName'])
359 self.assertEqual('http://ubuntu.com', new_ctx['MessageCenter'])
360 self.assertEqual('ubuntu:8080', new_ctx['MessageProxy'])
361 self.assertEqual('user', new_ctx['Username'])
362 self.assertEqual('pass', new_ctx['Password'])
363
364 def test_create_mms_and_internet_apn(self):
365 editor = self.cellular_page.open_apn_editor(None)
366 editor.set_type('internet+mms')
367 editor.set_name('Ubuntu')
368 editor.set_access_point_name('ubuntu.ap')
369 editor.set_message_center('ubuntu.com')
370 editor.set_message_proxy('ubuntu:8080')
371 editor.set_username('user')
372 editor.set_password('pass')
373 editor.save()
374
375 # Wait for our new context to appear.
376 self.assertThat(
377 lambda: self.modem_0.connMan.GetContexts()[0][0],
378 Eventually(Equals('/ril_0/context0'))
379 )
380 # Wait for our context to be renamed from the default ofono name.
381 self.assertThat(
382 lambda: self.modem_0.connMan.GetContexts()[0][1]['Name'],
383 Eventually(Equals('Ubuntu'))
384 )
385 contexts = self.modem_0.connMan.GetContexts()
386 new_ctx = contexts[0][1]
387 self.assertEqual(1, len(contexts))
388 self.assertEqual('internet', new_ctx['Type'])
389 self.assertEqual('ubuntu.ap', new_ctx['AccessPointName'])
390 self.assertEqual('http://ubuntu.com', new_ctx['MessageCenter'])
391 self.assertEqual('ubuntu:8080', new_ctx['MessageProxy'])
392 self.assertEqual('user', new_ctx['Username'])
393 self.assertEqual('pass', new_ctx['Password'])
394
395 def create_lte_apn(self):
396 editor = self.cellular_page.open_apn_editor(None)
397 editor.set_type('ia')
398 editor.set_name('Ubuntu')
399 editor.set_access_point_name('ubuntu.ap')
400 editor.set_username('user')
401 editor.set_password('pass')
402 editor.save()
403
404 # Wait for our new context to appear.
405 self.assertThat(
406 lambda: self.modem_0.connMan.GetContexts()[0][0],
407 Eventually(Equals('/ril_0/context0'))
408 )
409 # Wait for our context to be renamed from the default ofono name.
410 self.assertThat(
411 lambda: self.modem_0.connMan.GetContexts()[0][1]['Name'],
412 Eventually(Equals('Ubuntu'))
413 )
414 contexts = self.modem_0.connMan.GetContexts()
415 new_ctx = contexts[0][1]
416 self.assertEqual(1, len(contexts))
417 self.assertEqual('ia', new_ctx['Type'])
418 self.assertEqual('user', new_ctx['Username'])
419 self.assertEqual('pass', new_ctx['Password'])
420
421 def select_apn(self):
422 self.add_connection_context(self.modem_0,
423 Type='internet', Name='Provisioned')
424
425 self.cellular_page.prefer_apn('Provisioned')
426
427 # Assert that Preferred becomes true.
428 self.assertThat(
429 lambda: self.modem_0.connMan.GetContexts()[0][1]['Preferred'],
430 Eventually(Equals(True))
431 )

Subscribers

People subscribed via source and target branches