Merge lp:~uriboni/webbrowser-app/media-access into lp:webbrowser-app
- media-access
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Olivier Tilloy |
Approved revision: | 1222 |
Merged at revision: | 1246 |
Proposed branch: | lp:~uriboni/webbrowser-app/media-access |
Merge into: | lp:webbrowser-app |
Diff against target: |
709 lines (+508/-13) 9 files modified
debian/webbrowser-app-apparmor.manifest (+2/-0) src/app/webbrowser/Browser.qml (+30/-0) src/app/webbrowser/MediaAccessDialog.qml (+69/-0) src/app/webbrowser/SettingsDeviceSelector.qml (+71/-0) src/app/webbrowser/SettingsPage.qml (+97/-13) tests/autopilot/webbrowser_app/emulators/browser.py (+18/-0) tests/autopilot/webbrowser_app/tests/http_server.py (+10/-0) tests/autopilot/webbrowser_app/tests/test_media_access_permission.py (+98/-0) tests/unittests/qml/tst_SettingsPage.qml (+113/-0) |
To merge this branch: | bzr merge lp:~uriboni/webbrowser-app/media-access |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Approve | |
Olivier Tilloy | Approve | ||
Review via email: mp+272919@code.launchpad.net |
Commit message
Implement support for allowing or denying access to media input devices and for setting default media input devices.
Description of the change
Implement media access permissions support, prompting the user when the page requests access to the microphone or camera, and allowing the user to change their choices via new settings pages
Additionally allow the user to select the default audio and video input devices in the settings.
To test this on desktop if you don't have multiple webcams:
- sudo apt-get install v4l2loopback-dkms v4l2loopback-source
- sudo modprobe v4l2loopback
- gst-launch-0.10 gst-launch-0.10 videotestsrc ! v4l2sink device=/dev/video1
Replace video1 with the highest numbered video device in /dev/video*
Then in the settings you can switch between the sources. However to make the change effective you will need to cause the webpage to call again navigator.
Olivier Tilloy (osomon) wrote : | # |
Olivier Tilloy (osomon) wrote : | # |
You’re connecting to onMediaAccessPe
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1194
http://
Executed test runs:
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Ugo Riboni (uriboni) wrote : | # |
> You’re connecting to onMediaAccessPe
> What happens if another tab requests media access? Is the request lost?
If there is no handler connected to mediaAccessPerm
So when you go back to the page and cause navigator.
What I can try to do if this is not the desired behavior is to connect mediaAccessPerm
Olivier Tilloy (osomon) wrote : | # |
I don’t think holding up the request until the tab is focused is a good idea. Can you check how other mobile browsers behave in that regard? I would expect they either discard the request right away, or focus the tab that issues the request to make the user aware of the request.
Ugo Riboni (uriboni) wrote : | # |
> I don’t think holding up the request until the tab is focused is a good idea.
> Can you check how other mobile browsers behave in that regard? I would expect
> they either discard the request right away, or focus the tab that issues the
> request to make the user aware of the request.
Both firefox and chrome do something similar to what I suggested: they do nothing, and when you switch to the tab that has requested permission they pop up an authorization request. So it seems that internally, they are actually "holding" on to the request until the user has a chance to decide. However I don't think we can do that with the current implementation of Oxide.
Olivier Tilloy (osomon) wrote : | # |
> Both firefox and chrome do something similar to what I suggested: they do
> nothing, and when you switch to the tab that has requested permission they pop
> up an authorization request. So it seems that internally, they are actually
> "holding" on to the request until the user has a chance to decide. However I
> don't think we can do that with the current implementation of Oxide.
In that case discarding the request right away (by not connecting to the signal as you’re doing it) is probably the most sensible option. Can you please add a comment though, to make that explicit?
Olivier Tilloy (osomon) wrote : | # |
Can the "media access" entry in the settings go before the "reset browser settings" one?
"Media Access" should probably be spelled "Media access" to be consistent with other entries in the settings menu.
What about the informative message about needing to restart the browser for a domain permission to take effect, do we still want to display something like that?
Browsing to https:/
Similarly, if I do the above but instead of disallowing microphone access I forget the permission for this domain (by swiping to delete the entry), and after restarting the browser, I expect that the prompt would request microphone permission only (as camera was already granted). Instead, it prompts for both camera and microphone.
If I click anywhere outside the permission prompt, it is dismissed, but I have no idea whether the permission was granted or not. I think that dialog should be modal (or at least tab-modal) so it can’t be dismissed without explicitly choosing to allow or forbid media access.
Olivier Tilloy (osomon) wrote : | # |
I haven’t done a complete code review yet, but here’s a first round of comments:
For completeness, can you add to the class documentation of MediaAccessModel that its entries are not sorted in any particular order? (thanks for the detailed documentation btw, this is much appreciated)
In MediaAccessMode
isNullOrUndefined() should probably be enclosed in an anonymous namespace.
In MediaAccessMode
The same remark applies to ::unset().
The docstring for MediaAccessMode
In MediaAccessMode
Chris Coulson (chrisccoulson) wrote : | # |
I've had a brief look and left a comment inline.
The application shouldn't really be storing these decisions - it doesn't work properly for media access in some circumstances anyway because Chromium needs access to these decisions for navigator.
(It's even worse for notifications because a site has to explicitly request permission before sending a notification. This is how we ended up with a hack in Oxide to remember these decisions for the rest of a session).
Site-specific settings really belong in Oxide (see https:/
Olivier Tilloy (osomon) wrote : | # |
Although this is functionally fine, I’d prefer the databasePath of MediaAccessModel to be set declaratively rather than imperatively in Component.
Binding {
target: MediaAccessModel
property: "databasePath"
value: dataLocation + "/mediaAccess.
}
A media permission request’s 'origin' parameter includes the scheme, and I think this is important information that would should not be trimming when remembering permissions for an origin. A user might want to allow use of the camera for a given domain over an encrypted connection (e.g. origin='https:/
In MediaAccessDial
In SettingsPage.qml, the import of Qt.labs.settings can now be removed.
Olivier Tilloy (osomon) wrote : | # |
> The application shouldn't really be storing these decisions […]
Thanks for the review and pointers Chris.
So until this API is available in oxide, I guess the way to go for the browser is to pop up a permission request for every emission of mediaAccessPerm
This would be consistent with the way geolocation permission requests currently work in the browser. Slightly annoying for a user, but it will at least allow using camera and microphone, so definitely an improvement.
Ugo, can you rework that branch (or create a new one) to implement that?
Olivier Tilloy (osomon) wrote : | # |
The browser is now running under apparmor confinement, so we need to add the 'microphone' and 'camera' policy groups to debian/
Olivier Tilloy (osomon) wrote : | # |
And one additional point that Chris raised during our oxide weekly call: remembering permissions should only be done when browsing in public mode, incognito mode should not leave a trace of the user’s actions. No big deal now that this logic is moving to oxide, the incognito check will be handled there.
- 1195. By Ugo Riboni
-
Add support for selecting the default audio input device for the web context
- 1196. By Ugo Riboni
-
Move the media access settings within the privacy section and rename settings items according to design spec
- 1197. By Ugo Riboni
-
Add comment to explain why we are connecting the mediaAccessPerm
issionRequested only on the current webview - 1198. By Ugo Riboni
-
Add a note warning the user that they will need to restart the application for some changes to the media access permissions to be effective.
- 1199. By Ugo Riboni
-
Make the dialog an actual dialog, and modal
- 1200. By Ugo Riboni
-
Clarify and fix some MediaAccessModel documentation
- 1201. By Ugo Riboni
-
Update text of the restart note to match design
- 1202. By Ugo Riboni
-
Don't store permissions locally. Ask the user every time oxide sends a request
- 1203. By Ugo Riboni
-
Pop up a modal dialog when the active tab requests media access.
- 1204. By Ugo Riboni
-
Clarify via comments why we are doing things this way and what is the plan for the future
- 1205. By Ugo Riboni
-
Remove no longer used properties. Fix english question mark style
- 1206. By Ugo Riboni
-
Display information about the embedder (if any)
- 1207. By Ugo Riboni
-
Merge changes from trunk
- 1208. By Ugo Riboni
-
Fix text and positioning+size of buttons
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1206
http://
Executed test runs:
UNSTABLE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1208
http://
Executed test runs:
UNSTABLE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1209. By Ugo Riboni
-
Merge changes from trunk
- 1210. By Ugo Riboni
-
Add microphone and camera apparmor policy groups
Olivier Tilloy (osomon) wrote : | # |
In InputDevicesMod
What’s the use of InputDevicesMod
When running the autopilot tests, I’m seeing the following warning in the JS console:
[JS] (:0) getUserMedia() is deprecated on insecure origins, and support will be removed in the future. You should consider switching your application to a secure origin, such as HTTPS. See https:/
This is out of scope for this MR, but we need to think about supporting https in our autopilot tests.
webbrowser_
Traceback (most recent call last):
File "/home/
self.
File "/home/
webview.
File "/usr/lib/
failure_msg))
AssertionError: After 10.0 seconds test on WebViewImpl.url failed: 'http://
Could it be because I don’t have a webcam plugged into my laptop?
- 1211. By Ugo Riboni
-
Cleaner imports
- 1212. By Ugo Riboni
-
Remove InputDevicesMod
el.qml as it is not really useful - 1213. By Ugo Riboni
-
Alphabetically order apparmor policies
- 1214. By Ugo Riboni
-
Skip AP test as we can't guarantee a/v devices are present or test for their presence easily
- 1215. By Ugo Riboni
-
Use decorator to skip tests
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1210
http://
Executed test runs:
UNSTABLE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1215
http://
Executed test runs:
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
There’s now one minor conflict when merging this branch into the latest trunk, can you please resolve it?
Olivier Tilloy (osomon) wrote : | # |
Also note that after merging the latest trunk QmlTests:
2: QWARN : QmlTests:
2: historyModel: HistoryModelMock {
2: ^
- 1216. By Ugo Riboni
-
Merge changes from trunk
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1216
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
For consistency with the rest of the codebase, can you please remove the trailing semi-colons in JS code embedded in QML?
get_deny_button and get_allow_button are used only to have the tests click those buttons, maybe the methods could be transformed into click_*_button (with a corresponding autopilot.
While testing this branch on desktop, I realized that having a modal dialog displayed doesn’t inhibit keyboard shortcuts. As this isn’t specific to this branch, I filed bug #1507468 to track the issue, and we can address it separately. Just a FYI.
Olivier Tilloy (osomon) wrote : | # |
The rest of the changes look good. I’ve just tested on arale, and I’m not seeing any video devices listed in the settings (even with oxide 1.10.2 installed from the phablet-team PPA). Have you managed to validate that camera devices are listed on another device?
- 1217. By Ugo Riboni
-
Remove semicolons in JS
- 1218. By Ugo Riboni
-
Refactor AP tests to click the button directly from the emulator
Olivier Tilloy (osomon) wrote : | # |
There’s still one remaining trailing semi-colon in JS code (in Browser.qml).
The import of "../UrlUtils.js" in MediaAccessDial
In MediaAccessDial
In SettingsDeviceS
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1218
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1219. By Ugo Riboni
-
Kill all the semicolons
- 1220. By Ugo Riboni
-
Remove unused imports and ids
- 1221. By Ugo Riboni
-
Clarify explanation for translators
- 1222. By Ugo Riboni
-
Update the default device in one more where it is necessary
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1222
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1222
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Preview Diff
1 | === modified file 'debian/webbrowser-app-apparmor.manifest' |
2 | --- debian/webbrowser-app-apparmor.manifest 2015-09-29 20:14:26 +0000 |
3 | +++ debian/webbrowser-app-apparmor.manifest 2015-10-19 12:13:40 +0000 |
4 | @@ -9,10 +9,12 @@ |
5 | "policy_groups": [ |
6 | "accounts", |
7 | "audio", |
8 | + "camera", |
9 | "content_exchange", |
10 | "content_exchange_source", |
11 | "keep-display-on", |
12 | "location", |
13 | + "microphone", |
14 | "networking", |
15 | "push-notification-client", |
16 | "video", |
17 | |
18 | === modified file 'src/app/webbrowser/Browser.qml' |
19 | --- src/app/webbrowser/Browser.qml 2015-10-13 12:57:33 +0000 |
20 | +++ src/app/webbrowser/Browser.qml 2015-10-19 12:13:40 +0000 |
21 | @@ -75,6 +75,30 @@ |
22 | } |
23 | } |
24 | |
25 | + Connections { |
26 | + target: currentWebview |
27 | + |
28 | + /* Note that we are connecting the mediaAccessPermissionRequested signal |
29 | + on the current webview only because we want all the tabs that are not |
30 | + visible to automatically deny the request but emit the signal again |
31 | + if the same origin requests permissions (which is the default |
32 | + behavior in oxide if we don't connect a signal handler), so that we |
33 | + can pop-up a dialog asking the user for permission. |
34 | + |
35 | + Design is working on a new component that allows per-tab non-modal |
36 | + dialogs that will allow asking permission to the user without blocking |
37 | + interaction with the rest of the page or the window. When ready all |
38 | + tabs will have their mediaAccessPermissionRequested signal handled by |
39 | + creating one of these new dialogs. |
40 | + */ |
41 | + onMediaAccessPermissionRequested: PopupUtils.open(mediaAccessDialogComponent, null, { request: request }) |
42 | + } |
43 | + |
44 | + Component { |
45 | + id: mediaAccessDialogComponent |
46 | + MediaAccessDialog { } |
47 | + } |
48 | + |
49 | actions: [ |
50 | Actions.GoTo { |
51 | onTriggered: currentWebview.url = value |
52 | @@ -118,6 +142,8 @@ |
53 | property string allowOpenInBackgroundTab: settingsDefaults.allowOpenInBackgroundTab |
54 | property bool restoreSession: settingsDefaults.restoreSession |
55 | property int newTabDefaultSection: settingsDefaults.newTabDefaultSection |
56 | + property string defaultAudioDevice |
57 | + property string defaultVideoDevice |
58 | |
59 | function restoreDefaults() { |
60 | homepage = settingsDefaults.homepage |
61 | @@ -125,6 +151,8 @@ |
62 | allowOpenInBackgroundTab = settingsDefaults.allowOpenInBackgroundTab |
63 | restoreSession = settingsDefaults.restoreSession |
64 | newTabDefaultSection = settingsDefaults.newTabDefaultSection |
65 | + defaultAudioDevice = settingsDefaults.defaultAudioDevice |
66 | + defaultVideoDevice = settingsDefaults.defaultVideoDevice |
67 | } |
68 | } |
69 | |
70 | @@ -136,6 +164,8 @@ |
71 | readonly property string allowOpenInBackgroundTab: "default" |
72 | readonly property bool restoreSession: true |
73 | readonly property int newTabDefaultSection: 0 |
74 | + readonly property string defaultAudioDevice: "" |
75 | + readonly property string defaultVideoDevice: "" |
76 | } |
77 | |
78 | FocusScope { |
79 | |
80 | === added file 'src/app/webbrowser/MediaAccessDialog.qml' |
81 | --- src/app/webbrowser/MediaAccessDialog.qml 1970-01-01 00:00:00 +0000 |
82 | +++ src/app/webbrowser/MediaAccessDialog.qml 2015-10-19 12:13:40 +0000 |
83 | @@ -0,0 +1,69 @@ |
84 | +/* |
85 | + * Copyright 2015 Canonical Ltd. |
86 | + * |
87 | + * This file is part of webbrowser-app. |
88 | + * |
89 | + * webbrowser-app is free software; you can redistribute it and/or modify |
90 | + * it under the terms of the GNU General Public License as published by |
91 | + * the Free Software Foundation; version 3. |
92 | + * |
93 | + * webbrowser-app is distributed in the hope that it will be useful, |
94 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
95 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
96 | + * GNU General Public License for more details. |
97 | + * |
98 | + * You should have received a copy of the GNU General Public License |
99 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
100 | + */ |
101 | + |
102 | +import QtQuick 2.4 |
103 | +import Ubuntu.Components 1.3 |
104 | +import Ubuntu.Components.Popups 1.3 |
105 | + |
106 | +Dialog { |
107 | + property var request |
108 | + modal: true |
109 | + |
110 | + title: request.isForAudio && request.isForVideo ? |
111 | + i18n.tr("Allow this domain to access your camera and microphone?") : |
112 | + (request.isForVideo ? i18n.tr("Allow this domain to access your camera?") |
113 | + : i18n.tr("Allow this domain to access your microphone?")) |
114 | + |
115 | + text: request.embedder.toString() !== request.origin.toString() ? |
116 | + internal.textWhenEmbedded : request.origin |
117 | + |
118 | + Row { |
119 | + id: internal |
120 | + |
121 | + // TRANSLATORS: %1 is the URL of the site requesting access to camera and/or microphone and %2 is the URL of the site that embeds it |
122 | + readonly property string textWhenEmbedded: i18n.tr("%1 (embedded in %2)") |
123 | + .arg(request.origin).arg(request.embedder) |
124 | + height: units.gu(4) |
125 | + spacing: units.gu(2) |
126 | + anchors.horizontalCenter: parent.horizontalCenter |
127 | + |
128 | + Button { |
129 | + id: allowButton |
130 | + objectName: "mediaAccessDialog.allowButton" |
131 | + text: i18n.tr("Yes") |
132 | + color: UbuntuColors.green |
133 | + width: units.gu(14) |
134 | + onClicked: { |
135 | + request.allow() |
136 | + hide() |
137 | + } |
138 | + } |
139 | + |
140 | + Button { |
141 | + id: denyButton |
142 | + objectName: "mediaAccessDialog.denyButton" |
143 | + text: i18n.tr("No") |
144 | + color: UbuntuColors.red |
145 | + width: units.gu(14) |
146 | + onClicked: { |
147 | + request.deny() |
148 | + hide() |
149 | + } |
150 | + } |
151 | + } |
152 | +} |
153 | |
154 | === added file 'src/app/webbrowser/SettingsDeviceSelector.qml' |
155 | --- src/app/webbrowser/SettingsDeviceSelector.qml 1970-01-01 00:00:00 +0000 |
156 | +++ src/app/webbrowser/SettingsDeviceSelector.qml 2015-10-19 12:13:40 +0000 |
157 | @@ -0,0 +1,71 @@ |
158 | +/* |
159 | + * Copyright 2015 Canonical Ltd. |
160 | + * |
161 | + * This file is part of webbrowser-app. |
162 | + * |
163 | + * webbrowser-app is free software; you can redistribute it and/or modify |
164 | + * it under the terms of the GNU General Public License as published by |
165 | + * the Free Software Foundation; version 3. |
166 | + * |
167 | + * webbrowser-app is distributed in the hope that it will be useful, |
168 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
169 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
170 | + * GNU General Public License for more details. |
171 | + * |
172 | + * You should have received a copy of the GNU General Public License |
173 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
174 | + */ |
175 | + |
176 | +import QtQuick 2.4 |
177 | +import Ubuntu.Components 1.3 |
178 | +import com.canonical.Oxide 1.9 |
179 | + |
180 | +Item { |
181 | + property bool isAudio |
182 | + readonly property int devicesCount: internal.devices.length |
183 | + property alias enabled: selector.enabled |
184 | + property string defaultDevice |
185 | + signal deviceSelected(string id) |
186 | + |
187 | + implicitHeight: selector.height + units.gu(1) |
188 | + |
189 | + OptionSelector { |
190 | + id: selector |
191 | + |
192 | + anchors.top: parent.top |
193 | + anchors.left: parent.left |
194 | + anchors.right: parent.right |
195 | + anchors.margins: units.gu(1) |
196 | + containerHeight: itemHeight * model.length |
197 | + |
198 | + model: internal.devices |
199 | + delegate: OptionSelectorDelegate { text: modelData.id } |
200 | + onDelegateClicked: deviceSelected(model[index].id) |
201 | + } |
202 | + |
203 | + QtObject { |
204 | + id: internal |
205 | + |
206 | + property var devices: isAudio ? Oxide.availableAudioCaptureDevices : |
207 | + Oxide.availableVideoCaptureDevices |
208 | + |
209 | + function updateDefaultDevice() { |
210 | + for (var i = 0; i < devices.length; i++) { |
211 | + if (defaultDevice === devices[i].id) { |
212 | + selector.selectedIndex = i |
213 | + return |
214 | + } |
215 | + } |
216 | + } |
217 | + } |
218 | + |
219 | + onDefaultDeviceChanged: internal.updateDefaultDevice() |
220 | + Connections { |
221 | + target: Oxide |
222 | + onAvailableAudioCaptureDevicesChanged: if (isAudio) internal.updateDefaultDevice() |
223 | + onAvailableVideoCaptureDevicesChanged: if (!isAudio) internal.updateDefaultDevice() |
224 | + } |
225 | + |
226 | + onIsAudioChanged: internal.updateDefaultDevice() |
227 | +} |
228 | + |
229 | |
230 | === modified file 'src/app/webbrowser/SettingsPage.qml' |
231 | --- src/app/webbrowser/SettingsPage.qml 2015-09-01 15:35:40 +0000 |
232 | +++ src/app/webbrowser/SettingsPage.qml 2015-10-19 12:13:40 +0000 |
233 | @@ -20,7 +20,7 @@ |
234 | import Qt.labs.settings 1.0 |
235 | import Ubuntu.Components 1.3 |
236 | import Ubuntu.Components.Popups 1.3 |
237 | -import Ubuntu.Components.ListItems 1.3 as ListItem |
238 | +import Ubuntu.Components.ListItems 1.3 as ListItems |
239 | import Ubuntu.Web 0.2 |
240 | import webbrowserapp.private 0.1 |
241 | |
242 | @@ -29,7 +29,7 @@ |
243 | Item { |
244 | id: settingsItem |
245 | |
246 | - property Settings settingsObject |
247 | + property QtObject settingsObject |
248 | |
249 | signal done() |
250 | |
251 | @@ -68,7 +68,7 @@ |
252 | |
253 | width: parent.width |
254 | |
255 | - ListItem.Subtitled { |
256 | + ListItems.Subtitled { |
257 | objectName: "searchengine" |
258 | |
259 | SearchEngine { |
260 | @@ -85,7 +85,7 @@ |
261 | onClicked: searchEngineComponent.createObject(subpageContainer) |
262 | } |
263 | |
264 | - ListItem.Subtitled { |
265 | + ListItems.Subtitled { |
266 | objectName: "homepage" |
267 | |
268 | text: i18n.tr("Homepage") |
269 | @@ -94,7 +94,7 @@ |
270 | onClicked: PopupUtils.open(homepageDialog) |
271 | } |
272 | |
273 | - ListItem.Standard { |
274 | + ListItems.Standard { |
275 | objectName: "restoreSession" |
276 | |
277 | text: i18n.tr("Restore previous session at startup") |
278 | @@ -112,7 +112,7 @@ |
279 | } |
280 | } |
281 | |
282 | - ListItem.Standard { |
283 | + ListItems.Standard { |
284 | objectName: "backgroundTabs" |
285 | |
286 | text: i18n.tr("Allow opening new tabs in background") |
287 | @@ -131,15 +131,15 @@ |
288 | } |
289 | } |
290 | |
291 | - ListItem.Standard { |
292 | + ListItems.Standard { |
293 | objectName: "privacy" |
294 | |
295 | - text: i18n.tr("Privacy") |
296 | + text: i18n.tr("Privacy & permissions") |
297 | |
298 | onClicked: privacyComponent.createObject(subpageContainer) |
299 | } |
300 | |
301 | - ListItem.Standard { |
302 | + ListItems.Standard { |
303 | objectName: "reset" |
304 | |
305 | text: i18n.tr("Reset browser settings") |
306 | @@ -186,7 +186,7 @@ |
307 | |
308 | model: searchEngines.engines |
309 | |
310 | - delegate: ListItem.Standard { |
311 | + delegate: ListItems.Standard { |
312 | objectName: "searchEngineDelegate_" + index |
313 | SearchEngine { |
314 | id: searchEngineDelegate |
315 | @@ -224,7 +224,7 @@ |
316 | SettingsPageHeader { |
317 | id: privacyTitle |
318 | onBack: privacyItem.destroy() |
319 | - text: i18n.tr("Privacy") |
320 | + text: i18n.tr("Privacy & permissions") |
321 | } |
322 | |
323 | Flickable { |
324 | @@ -243,7 +243,13 @@ |
325 | id: privacyCol |
326 | width: parent.width |
327 | |
328 | - ListItem.Standard { |
329 | + ListItems.Standard { |
330 | + objectName: "privacy.mediaAccess" |
331 | + text: i18n.tr("Camera & microphone") |
332 | + onClicked: mediaAccessComponent.createObject(subpageContainer) |
333 | + } |
334 | + |
335 | + ListItems.Standard { |
336 | objectName: "privacy.clearHistory" |
337 | text: i18n.tr("Clear Browsing History") |
338 | enabled: HistoryModel.count > 0 |
339 | @@ -253,7 +259,7 @@ |
340 | } |
341 | } |
342 | |
343 | - ListItem.Standard { |
344 | + ListItems.Standard { |
345 | objectName: "privacy.clearCache" |
346 | text: i18n.tr("Clear Cache") |
347 | onClicked: { |
348 | @@ -359,5 +365,83 @@ |
349 | } |
350 | } |
351 | } |
352 | + |
353 | + Component { |
354 | + id: mediaAccessComponent |
355 | + |
356 | + Item { |
357 | + id: mediaAccessItem |
358 | + objectName: "mediaAccessSettings" |
359 | + anchors.fill: parent |
360 | + |
361 | + Rectangle { |
362 | + anchors.fill: parent |
363 | + color: "#f6f6f6" |
364 | + } |
365 | + |
366 | + SettingsPageHeader { |
367 | + id: mediaAccessTitle |
368 | + |
369 | + onBack: mediaAccessItem.destroy() |
370 | + text: i18n.tr("Camera & microphone") |
371 | + } |
372 | + |
373 | + Flickable { |
374 | + anchors { |
375 | + top: mediaAccessTitle.bottom |
376 | + left: parent.left |
377 | + right: parent.right |
378 | + bottom: parent.bottom |
379 | + } |
380 | + |
381 | + clip: true |
382 | + |
383 | + contentHeight: mediaAccessCol.height |
384 | + |
385 | + Column { |
386 | + id: mediaAccessCol |
387 | + width: parent.width |
388 | + |
389 | + ListItems.Standard { |
390 | + text: i18n.tr("Microphone") |
391 | + } |
392 | + |
393 | + SettingsDeviceSelector { |
394 | + anchors.left: parent.left |
395 | + anchors.right: parent.right |
396 | + |
397 | + isAudio: true |
398 | + visible: devicesCount > 0 |
399 | + enabled: devicesCount > 1 |
400 | + |
401 | + defaultDevice: settings.defaultAudioDevice |
402 | + onDeviceSelected: { |
403 | + SharedWebContext.sharedContext.defaultAudioCaptureDeviceId = id |
404 | + settings.defaultAudioDevice = id |
405 | + } |
406 | + } |
407 | + |
408 | + ListItems.Standard { |
409 | + text: i18n.tr("Camera") |
410 | + } |
411 | + |
412 | + SettingsDeviceSelector { |
413 | + anchors.left: parent.left |
414 | + anchors.right: parent.right |
415 | + |
416 | + isAudio: false |
417 | + visible: devicesCount > 0 |
418 | + enabled: devicesCount > 1 |
419 | + |
420 | + defaultDevice: settings.defaultVideoDevice |
421 | + onDeviceSelected: { |
422 | + SharedWebContext.sharedContext.defaultVideoCaptureDeviceId = id |
423 | + settings.defaultVideoDevice = id |
424 | + } |
425 | + } |
426 | + } |
427 | + } |
428 | + } |
429 | + } |
430 | } |
431 | |
432 | |
433 | === modified file 'tests/autopilot/webbrowser_app/emulators/browser.py' |
434 | --- tests/autopilot/webbrowser_app/emulators/browser.py 2015-10-08 15:26:18 +0000 |
435 | +++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-10-19 12:13:40 +0000 |
436 | @@ -123,6 +123,9 @@ |
437 | def get_http_auth_dialog(self): |
438 | return self.wait_select_single(HttpAuthenticationDialog) |
439 | |
440 | + def get_media_access_dialog(self): |
441 | + return self.wait_select_single(MediaAccessDialog) |
442 | + |
443 | def get_tabs_view(self): |
444 | return self.wait_select_single(TabsList, visible=True) |
445 | |
446 | @@ -402,6 +405,21 @@ |
447 | return self.select_single("TextField", objectName="password") |
448 | |
449 | |
450 | +class MediaAccessDialog(uitk.UbuntuUIToolkitCustomProxyObjectBase): |
451 | + |
452 | + @autopilot.logging.log_action(logger.info) |
453 | + def click_deny_button(self): |
454 | + button = self.select_single("Button", |
455 | + objectName="mediaAccessDialog.denyButton") |
456 | + self.pointing_device.click_object(button) |
457 | + |
458 | + @autopilot.logging.log_action(logger.info) |
459 | + def click_allow_button(self): |
460 | + button = self.select_single("Button", |
461 | + objectName="mediaAccessDialog.allowButton") |
462 | + self.pointing_device.click_object(button) |
463 | + |
464 | + |
465 | class TabPreview(uitk.UbuntuUIToolkitCustomProxyObjectBase): |
466 | |
467 | @autopilot.logging.log_action(logger.info) |
468 | |
469 | === modified file 'tests/autopilot/webbrowser_app/tests/http_server.py' |
470 | --- tests/autopilot/webbrowser_app/tests/http_server.py 2015-09-15 15:21:15 +0000 |
471 | +++ tests/autopilot/webbrowser_app/tests/http_server.py 2015-10-19 12:13:40 +0000 |
472 | @@ -193,6 +193,16 @@ |
473 | self.send_auth_request() |
474 | else: |
475 | self.send_auth_request() |
476 | + elif self.path.startswith("/media/"): |
477 | + self.send_response(200) |
478 | + permissions = self.path[len("/media/"):] |
479 | + self.send_html( |
480 | + "<script>navigator.webkitGetUserMedia(" |
481 | + "{video: " + ("true" if "v" in permissions else "false") + |
482 | + ", audio: " + ("true" if "a" in permissions else "false") + |
483 | + "}, function() { location.href = '/test1' } " + |
484 | + ", function() { location.href = '/test2' })</script>" |
485 | + ) |
486 | else: |
487 | self.send_error(404) |
488 | |
489 | |
490 | === added file 'tests/autopilot/webbrowser_app/tests/test_media_access_permission.py' |
491 | --- tests/autopilot/webbrowser_app/tests/test_media_access_permission.py 1970-01-01 00:00:00 +0000 |
492 | +++ tests/autopilot/webbrowser_app/tests/test_media_access_permission.py 2015-10-19 12:13:40 +0000 |
493 | @@ -0,0 +1,98 @@ |
494 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
495 | +# |
496 | +# Copyright 2015 Canonical |
497 | +# |
498 | +# This program is free software: you can redistribute it and/or modify it |
499 | +# under the terms of the GNU General Public License version 3, as published |
500 | +# by the Free Software Foundation. |
501 | +# |
502 | +# This program is distributed in the hope that it will be useful, |
503 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
504 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
505 | +# GNU General Public License for more details. |
506 | +# |
507 | +# You should have received a copy of the GNU General Public License |
508 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
509 | + |
510 | +import testtools |
511 | +from webbrowser_app.tests import StartOpenRemotePageTestCaseBase |
512 | + |
513 | + |
514 | +class TestMediaAccessPermission(StartOpenRemotePageTestCaseBase): |
515 | + |
516 | + def setUp(self): |
517 | + super(TestMediaAccessPermission, self).setUp() |
518 | + self.url = self.base_url + "/media/" |
519 | + self.allowed_url = self.base_url + "/test1" |
520 | + self.denied_url = self.base_url + "/test2" |
521 | + |
522 | + @testtools.skip("We can't guarantee/test that audio/video devices exist") |
523 | + def test_allow(self): |
524 | + # verify that trying to access any media raises an authorization dialog |
525 | + url = self.url + "a" |
526 | + self.main_window.go_to_url(url) |
527 | + self.main_window.wait_until_page_loaded(url) |
528 | + dialog = self.main_window.get_media_access_dialog() |
529 | + |
530 | + # note that we have no easy way to verify that the browser actually |
531 | + # grants or denied permission based on our choice, because we can't |
532 | + # easily inspect the contents of the page from AP tests. the simplest |
533 | + # workaround I could find was to redirect the user to two different |
534 | + # pages upon permission granted or denied, and detect that instead |
535 | + dialog.click_allow_button() |
536 | + dialog.wait_until_destroyed() |
537 | + self.main_window.wait_until_page_loaded(self.allowed_url) |
538 | + |
539 | + # verify that trying to access the same media for the same origin in |
540 | + # the same session will not ask for permission again... |
541 | + self.main_window.go_to_url(url) |
542 | + self.main_window.wait_until_page_loaded(self.allowed_url) |
543 | + |
544 | + # ...but it will ask if we try to access other media |
545 | + url = self.url + "v" |
546 | + self.main_window.go_to_url(url) |
547 | + self.main_window.wait_until_page_loaded(url) |
548 | + dialog = self.main_window.get_media_access_dialog() |
549 | + dialog.click_allow_button() |
550 | + dialog.wait_until_destroyed() |
551 | + self.main_window.wait_until_page_loaded(self.allowed_url) |
552 | + |
553 | + # now that we granted both permissions, verify that asking for both |
554 | + # together will also not raise the dialog |
555 | + url = self.url + "av" |
556 | + self.main_window.go_to_url(url) |
557 | + self.main_window.wait_until_page_loaded(self.allowed_url) |
558 | + |
559 | + def test_deny(self): |
560 | + # verify that trying to access any media raises an authorization dialog |
561 | + # and we get redirected to the denial page in case we refuse to give |
562 | + # permission |
563 | + url = self.url + "a" |
564 | + self.main_window.go_to_url(url) |
565 | + self.main_window.wait_until_page_loaded(url) |
566 | + dialog = self.main_window.get_media_access_dialog() |
567 | + dialog.click_deny_button() |
568 | + dialog.wait_until_destroyed() |
569 | + self.main_window.wait_until_page_loaded(self.denied_url) |
570 | + |
571 | + # verify that trying to access the same media for the same origin in |
572 | + # the same session will not ask for permission again... |
573 | + self.main_window.go_to_url(url) |
574 | + self.main_window.wait_until_page_loaded(self.denied_url) |
575 | + |
576 | + @testtools.skip("Skipping due to oxide bug, see http://pad.lv/1501017") |
577 | + def test_deny_combined(self): |
578 | + # deny first one input type, then try to ask both and verify that a |
579 | + # request is made for the media that was not asked for the fist time |
580 | + url = self.url + "a" |
581 | + self.main_window.go_to_url(url) |
582 | + self.main_window.wait_until_page_loaded(url) |
583 | + dialog = self.main_window.get_media_access_dialog() |
584 | + dialog.click_deny_button() |
585 | + dialog.wait_until_destroyed() |
586 | + self.main_window.wait_until_page_loaded(self.denied_url) |
587 | + |
588 | + url = self.url + "av" |
589 | + self.main_window.go_to_url(url) |
590 | + self.main_window.wait_until_page_loaded(url) |
591 | + dialog = self.main_window.get_media_access_dialog() |
592 | |
593 | === added file 'tests/unittests/qml/tst_SettingsPage.qml' |
594 | --- tests/unittests/qml/tst_SettingsPage.qml 1970-01-01 00:00:00 +0000 |
595 | +++ tests/unittests/qml/tst_SettingsPage.qml 2015-10-19 12:13:40 +0000 |
596 | @@ -0,0 +1,113 @@ |
597 | +/* |
598 | + * Copyright 2015 Canonical Ltd. |
599 | + * |
600 | + * This file is part of webbrowser-app. |
601 | + * |
602 | + * webbrowser-app is free software; you can redistribute it and/or modify |
603 | + * it under the terms of the GNU General Public License as published by |
604 | + * the Free Software Foundation; version 3. |
605 | + * |
606 | + * webbrowser-app is distributed in the hope that it will be useful, |
607 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
608 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
609 | + * GNU General Public License for more details. |
610 | + * |
611 | + * You should have received a copy of the GNU General Public License |
612 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
613 | + */ |
614 | + |
615 | +import QtQuick 2.4 |
616 | +import QtTest 1.0 |
617 | +import Ubuntu.Test 1.0 |
618 | +import webbrowserapp.private 0.1 |
619 | +import webbrowsertest.private 0.1 |
620 | +import "../../../src/app/webbrowser" |
621 | + |
622 | +Item { |
623 | + width: 400 |
624 | + height: 600 |
625 | + |
626 | + property var settingsPage: settingsPageLoader.item |
627 | + |
628 | + Loader { |
629 | + id: settingsPageLoader |
630 | + anchors.fill: parent |
631 | + active: false |
632 | + sourceComponent: SettingsPage { |
633 | + anchors.fill: parent |
634 | + |
635 | + // NOTE: the following properties are not necessary for the tests |
636 | + // currently in this file, but if we don't provide them a lot of |
637 | + // warnings will be generated. |
638 | + // Ideally either more tests that use them will be added or the code |
639 | + // in SettingsPage will be refactored to cope with the missing |
640 | + // settings. |
641 | + settingsObject: QtObject { |
642 | + property url homepage |
643 | + property string searchEngine |
644 | + property int newTabDefaultSection: 0 |
645 | + } |
646 | + } |
647 | + } |
648 | + |
649 | + UbuntuTestCase { |
650 | + name: "TestSettingsPage" |
651 | + when: windowShown |
652 | + |
653 | + function clickItem(item) { |
654 | + var center = centerOf(item) |
655 | + mouseClick(item, center.x, center.y) |
656 | + } |
657 | + |
658 | + function swipeItemRight(item) { |
659 | + var center = centerOf(item) |
660 | + var dx = item.width * 0.5 |
661 | + mousePress(item, center.x, center.y) |
662 | + mouseMoveSlowly(item, center.x, center.y, dx, 0, 10, 0.01) |
663 | + mouseRelease(item, center.x + dx, center.y) |
664 | + } |
665 | + |
666 | + function init() { |
667 | + settingsPageLoader.active = true |
668 | + waitForRendering(settingsPageLoader.item) |
669 | + } |
670 | + |
671 | + function cleanup() { |
672 | + settingsPageLoader.active = false |
673 | + } |
674 | + |
675 | + function getListItems(name, itemName) { |
676 | + var list = findChild(settingsPage, name) |
677 | + var items = [] |
678 | + if (list) { |
679 | + // ensure all the delegates are created |
680 | + list.cacheBuffer = list.count * 1000 |
681 | + |
682 | + // In some cases the ListView might add other children to the |
683 | + // contentItem, so we filter the list of children to include |
684 | + // only actual delegates (names for delegates in this case |
685 | + // follow the pattern "name_index") |
686 | + var children = list.contentItem.children |
687 | + for (var i = 0; i < children.length; i++) { |
688 | + if (children[i].objectName.indexOf(itemName) == 0) { |
689 | + items.push(children[i]) |
690 | + } |
691 | + } |
692 | + } |
693 | + return items |
694 | + } |
695 | + |
696 | + function activateSettingsItem(itemName, pageName) { |
697 | + var item = findChild(settingsPage, itemName) |
698 | + clickItem(item) |
699 | + var page = findChild(settingsPage, pageName) |
700 | + waitForRendering(page) |
701 | + return page |
702 | + } |
703 | + |
704 | + function goToMediaAccessPage() { |
705 | + activateSettingsItem("privacy", "privacySettings") |
706 | + return activateSettingsItem("privacy.mediaAccess", "mediaAccessSettings") |
707 | + } |
708 | + } |
709 | +} |
Thanks for your work on this. I haven’t had a chance to test/review yet, but here is a preliminary comment: if/when https:/ /code.launchpad .net/~osomon/ webbrowser- app/apparmor- profile/ +merge/ 272850 lands, we will need to add the 'microphone' and 'camera' policy groups to debian/ webbrowser- app-apparmor. manifest.