Merge lp:~uriboni/webbrowser-app/media-access into lp:webbrowser-app

Proposed by Ugo Riboni
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
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.getUserMedia to pickup the new default stream(s).

To post a comment you must log in.
Revision history for this message
Olivier Tilloy (osomon) wrote :

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.

Revision history for this message
Olivier Tilloy (osomon) wrote :

You’re connecting to onMediaAccessPermissionRequested for the current webview. What happens if another tab requests media access? Is the request lost?

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

> You’re connecting to onMediaAccessPermissionRequested for the current webview.
> What happens if another tab requests media access? Is the request lost?

If there is no handler connected to mediaAccessPermissionRequested then oxide will reject the request, but it will also not remember that choice internally for the session.
So when you go back to the page and cause navigator.getUserMedia to be called again by the page, a new onMediaAccessPermissionRequested call will be made, and webbrowser-app will be able to handle it.

What I can try to do if this is not the desired behavior is to connect mediaAccessPermissionRequested to all open webviews, and if the user has already granted/denied permission then go ahead and respond with that information to the request. Otherwise I can try to "hold" the request pending and display a dialog with the permission request as soon as the user switches back to the page. Though I am not sure yet if this is possible, because as soon as the mediaAccessPermissionRequested handler returns, oxide will temporarily deny the request.

Revision history for this message
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.

Revision history for this message
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.

Revision history for this message
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?

Revision history for this message
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://opentokrtc.com/foobarbaz, allowing both camera and microphone access, then I go to settings and disallow microphone access for that domain, restart the browser and I’m prompted for both permissions again. I would expect not to be prompted at all.

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.

review: Needs Fixing (functional)
Revision history for this message
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 MediaAccessModel::data(), is the explicit cast to bool really needed?

isNullOrUndefined() should probably be enclosed in an anonymous namespace.

In MediaAccessModel::set(), writes to the DB could be done after signals (dataChanged, rowsInserted, rowCountChanged) have been emitted. This would have the potential to make the UI more responsive by not tying it to disk access (and would be consistent with how other SQLite-DB-backed models behave).
The same remark applies to ::unset().

The docstring for MediaAccessModel::unset() starts with "\qmlmethod void set", it should be "\qmlmethod void unset".

In MediaAccessModel::unset(), if an origin has e.g. only audio set, and I request unsetting only video, it looks to me like a dataChanged signal will be emitted (and the database will be written to), while in fact nothing should happen.

Revision history for this message
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.mediaDevices.enumerateDevices to work properly (the device names are scrubbed unless permission has already been granted to a domain).

(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://blueprints.launchpad.net/oxide/+spec/site-settings, and https://docs.google.com/document/d/1EZV4KzrH9eEUMjh3G8Cdykw3JTBtqOlunEEZd1kothA/edit)

Revision history for this message
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.onCompleted. You could do something like that:

    Binding {
        target: MediaAccessModel
        property: "databasePath"
        value: dataLocation + "/mediaAccess.sqlite"
    }

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://webrtc.foobar.org/baz') but not over an unencrypted connection (e.g. origin='http://webrtc.foobar.org/bleh').

In MediaAccessDialog.qml, there are extraneous whitespaces between the end of a sentence and the question mark that closes them, in user-facing strings.

In SettingsPage.qml, the import of Qt.labs.settings can now be removed.

review: Needs Fixing
Revision history for this message
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 mediaAccessPermissionRequested, right?
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?

Revision history for this message
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/webbrowser-app-apparmor.manifest.

Revision history for this message
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 mediaAccessPermissionRequested 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

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)
1209. By Ugo Riboni

Merge changes from trunk

1210. By Ugo Riboni

Add microphone and camera apparmor policy groups

Revision history for this message
Olivier Tilloy (osomon) wrote :

In InputDevicesModel.qml, to avoid the double "Oxide." prefix, you can simply import com.canonical.Oxide 1.9 as an unnamed import.

What’s the use of InputDevicesModel.qml? Oxide.available{Audio,Video}CaptureDevices is a list of variants, do we really need to convert that to an array of string ids? Can’t we have direct references to Oxide.available{Audio,Video}CaptureDevices in SettingsDeviceSelector.qml ?

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://goo.gl/rStTGz for more details.
This is out of scope for this MR, but we need to think about supporting https in our autopilot tests.

webbrowser_app.tests.test_media_access_permission.TestMediaAccessPermission.test_allow fails locally with the following traceback:

Traceback (most recent call last):
  File "/home/osomon/dev/phablet/browser/webbrowser-app/tests/autopilot/webbrowser_app/tests/test_media_access_permission.py", line 52, in test_allow
    self.main_window.wait_until_page_loaded(url)
  File "/home/osomon/dev/phablet/browser/webbrowser-app/tests/autopilot/webbrowser_app/emulators/browser.py", line 45, in wait_until_page_loaded
    webview.url.wait_for(url)
  File "/usr/lib/python3/dist-packages/autopilot/introspection/types.py", line 180, in wait_for
    failure_msg))
AssertionError: After 10.0 seconds test on WebViewImpl.url failed: 'http://test/media/v' != dbus.String('http://test/test2', variant_level=1)

Could it be because I don’t have a webcam plugged into my laptop?

review: Needs Fixing
1211. By Ugo Riboni

Cleaner imports

1212. By Ugo Riboni

Remove InputDevicesModel.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

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)
Revision history for this message
Olivier Tilloy (osomon) wrote :

There’s now one minor conflict when merging this branch into the latest trunk, can you please resolve it?

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

Also note that after merging the latest trunk QmlTests::tst_SettingsPage will need to be updated, it fails with the following error:

2: QWARN : QmlTests::UnknownTestFunc() file:///home/osomon/dev/phablet/browser/webbrowser-app/tests/unittests/qml/tst_SettingsPage.qml:45:27: HistoryModelMock is not a type
2: historyModel: HistoryModelMock {
2: ^

1216. By Ugo Riboni

Merge changes from trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
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.logging.log_action decorator, see other similar methods in browser.py).

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.

Revision history for this message
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

Revision history for this message
Olivier Tilloy (osomon) wrote :

There’s still one remaining trailing semi-colon in JS code (in Browser.qml).

The import of "../UrlUtils.js" in MediaAccessDialog.qml is unused. So is "id: dialog".

In MediaAccessDialog.qml, the comment for translators could be more specific (e.g. "requesting access *to the microphone/camera*"). Keep in mind that translators are not expected to go read the code to figure out the context of a string.

In SettingsDeviceSelector.qml, shouldn’t updateDefaultDevice() be called when isAudio changes, too?

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
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

Revision history for this message
Olivier Tilloy (osomon) wrote :

LGTM.

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: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'debian/webbrowser-app-apparmor.manifest'
--- debian/webbrowser-app-apparmor.manifest 2015-09-29 20:14:26 +0000
+++ debian/webbrowser-app-apparmor.manifest 2015-10-19 12:13:40 +0000
@@ -9,10 +9,12 @@
9 "policy_groups": [9 "policy_groups": [
10 "accounts",10 "accounts",
11 "audio",11 "audio",
12 "camera",
12 "content_exchange",13 "content_exchange",
13 "content_exchange_source",14 "content_exchange_source",
14 "keep-display-on",15 "keep-display-on",
15 "location",16 "location",
17 "microphone",
16 "networking",18 "networking",
17 "push-notification-client",19 "push-notification-client",
18 "video",20 "video",
1921
=== modified file 'src/app/webbrowser/Browser.qml'
--- src/app/webbrowser/Browser.qml 2015-10-13 12:57:33 +0000
+++ src/app/webbrowser/Browser.qml 2015-10-19 12:13:40 +0000
@@ -75,6 +75,30 @@
75 }75 }
76 }76 }
7777
78 Connections {
79 target: currentWebview
80
81 /* Note that we are connecting the mediaAccessPermissionRequested signal
82 on the current webview only because we want all the tabs that are not
83 visible to automatically deny the request but emit the signal again
84 if the same origin requests permissions (which is the default
85 behavior in oxide if we don't connect a signal handler), so that we
86 can pop-up a dialog asking the user for permission.
87
88 Design is working on a new component that allows per-tab non-modal
89 dialogs that will allow asking permission to the user without blocking
90 interaction with the rest of the page or the window. When ready all
91 tabs will have their mediaAccessPermissionRequested signal handled by
92 creating one of these new dialogs.
93 */
94 onMediaAccessPermissionRequested: PopupUtils.open(mediaAccessDialogComponent, null, { request: request })
95 }
96
97 Component {
98 id: mediaAccessDialogComponent
99 MediaAccessDialog { }
100 }
101
78 actions: [102 actions: [
79 Actions.GoTo {103 Actions.GoTo {
80 onTriggered: currentWebview.url = value104 onTriggered: currentWebview.url = value
@@ -118,6 +142,8 @@
118 property string allowOpenInBackgroundTab: settingsDefaults.allowOpenInBackgroundTab142 property string allowOpenInBackgroundTab: settingsDefaults.allowOpenInBackgroundTab
119 property bool restoreSession: settingsDefaults.restoreSession143 property bool restoreSession: settingsDefaults.restoreSession
120 property int newTabDefaultSection: settingsDefaults.newTabDefaultSection144 property int newTabDefaultSection: settingsDefaults.newTabDefaultSection
145 property string defaultAudioDevice
146 property string defaultVideoDevice
121147
122 function restoreDefaults() {148 function restoreDefaults() {
123 homepage = settingsDefaults.homepage149 homepage = settingsDefaults.homepage
@@ -125,6 +151,8 @@
125 allowOpenInBackgroundTab = settingsDefaults.allowOpenInBackgroundTab151 allowOpenInBackgroundTab = settingsDefaults.allowOpenInBackgroundTab
126 restoreSession = settingsDefaults.restoreSession152 restoreSession = settingsDefaults.restoreSession
127 newTabDefaultSection = settingsDefaults.newTabDefaultSection153 newTabDefaultSection = settingsDefaults.newTabDefaultSection
154 defaultAudioDevice = settingsDefaults.defaultAudioDevice
155 defaultVideoDevice = settingsDefaults.defaultVideoDevice
128 }156 }
129 }157 }
130158
@@ -136,6 +164,8 @@
136 readonly property string allowOpenInBackgroundTab: "default"164 readonly property string allowOpenInBackgroundTab: "default"
137 readonly property bool restoreSession: true165 readonly property bool restoreSession: true
138 readonly property int newTabDefaultSection: 0166 readonly property int newTabDefaultSection: 0
167 readonly property string defaultAudioDevice: ""
168 readonly property string defaultVideoDevice: ""
139 }169 }
140170
141 FocusScope {171 FocusScope {
142172
=== added file 'src/app/webbrowser/MediaAccessDialog.qml'
--- src/app/webbrowser/MediaAccessDialog.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/MediaAccessDialog.qml 2015-10-19 12:13:40 +0000
@@ -0,0 +1,69 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21import Ubuntu.Components.Popups 1.3
22
23Dialog {
24 property var request
25 modal: true
26
27 title: request.isForAudio && request.isForVideo ?
28 i18n.tr("Allow this domain to access your camera and microphone?") :
29 (request.isForVideo ? i18n.tr("Allow this domain to access your camera?")
30 : i18n.tr("Allow this domain to access your microphone?"))
31
32 text: request.embedder.toString() !== request.origin.toString() ?
33 internal.textWhenEmbedded : request.origin
34
35 Row {
36 id: internal
37
38 // 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
39 readonly property string textWhenEmbedded: i18n.tr("%1 (embedded in %2)")
40 .arg(request.origin).arg(request.embedder)
41 height: units.gu(4)
42 spacing: units.gu(2)
43 anchors.horizontalCenter: parent.horizontalCenter
44
45 Button {
46 id: allowButton
47 objectName: "mediaAccessDialog.allowButton"
48 text: i18n.tr("Yes")
49 color: UbuntuColors.green
50 width: units.gu(14)
51 onClicked: {
52 request.allow()
53 hide()
54 }
55 }
56
57 Button {
58 id: denyButton
59 objectName: "mediaAccessDialog.denyButton"
60 text: i18n.tr("No")
61 color: UbuntuColors.red
62 width: units.gu(14)
63 onClicked: {
64 request.deny()
65 hide()
66 }
67 }
68 }
69}
070
=== added file 'src/app/webbrowser/SettingsDeviceSelector.qml'
--- src/app/webbrowser/SettingsDeviceSelector.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/SettingsDeviceSelector.qml 2015-10-19 12:13:40 +0000
@@ -0,0 +1,71 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21import com.canonical.Oxide 1.9
22
23Item {
24 property bool isAudio
25 readonly property int devicesCount: internal.devices.length
26 property alias enabled: selector.enabled
27 property string defaultDevice
28 signal deviceSelected(string id)
29
30 implicitHeight: selector.height + units.gu(1)
31
32 OptionSelector {
33 id: selector
34
35 anchors.top: parent.top
36 anchors.left: parent.left
37 anchors.right: parent.right
38 anchors.margins: units.gu(1)
39 containerHeight: itemHeight * model.length
40
41 model: internal.devices
42 delegate: OptionSelectorDelegate { text: modelData.id }
43 onDelegateClicked: deviceSelected(model[index].id)
44 }
45
46 QtObject {
47 id: internal
48
49 property var devices: isAudio ? Oxide.availableAudioCaptureDevices :
50 Oxide.availableVideoCaptureDevices
51
52 function updateDefaultDevice() {
53 for (var i = 0; i < devices.length; i++) {
54 if (defaultDevice === devices[i].id) {
55 selector.selectedIndex = i
56 return
57 }
58 }
59 }
60 }
61
62 onDefaultDeviceChanged: internal.updateDefaultDevice()
63 Connections {
64 target: Oxide
65 onAvailableAudioCaptureDevicesChanged: if (isAudio) internal.updateDefaultDevice()
66 onAvailableVideoCaptureDevicesChanged: if (!isAudio) internal.updateDefaultDevice()
67 }
68
69 onIsAudioChanged: internal.updateDefaultDevice()
70}
71
072
=== modified file 'src/app/webbrowser/SettingsPage.qml'
--- src/app/webbrowser/SettingsPage.qml 2015-09-01 15:35:40 +0000
+++ src/app/webbrowser/SettingsPage.qml 2015-10-19 12:13:40 +0000
@@ -20,7 +20,7 @@
20import Qt.labs.settings 1.020import Qt.labs.settings 1.0
21import Ubuntu.Components 1.321import Ubuntu.Components 1.3
22import Ubuntu.Components.Popups 1.322import Ubuntu.Components.Popups 1.3
23import Ubuntu.Components.ListItems 1.3 as ListItem23import Ubuntu.Components.ListItems 1.3 as ListItems
24import Ubuntu.Web 0.224import Ubuntu.Web 0.2
25import webbrowserapp.private 0.125import webbrowserapp.private 0.1
2626
@@ -29,7 +29,7 @@
29Item {29Item {
30 id: settingsItem30 id: settingsItem
3131
32 property Settings settingsObject32 property QtObject settingsObject
3333
34 signal done()34 signal done()
3535
@@ -68,7 +68,7 @@
6868
69 width: parent.width69 width: parent.width
7070
71 ListItem.Subtitled {71 ListItems.Subtitled {
72 objectName: "searchengine"72 objectName: "searchengine"
7373
74 SearchEngine {74 SearchEngine {
@@ -85,7 +85,7 @@
85 onClicked: searchEngineComponent.createObject(subpageContainer)85 onClicked: searchEngineComponent.createObject(subpageContainer)
86 }86 }
8787
88 ListItem.Subtitled {88 ListItems.Subtitled {
89 objectName: "homepage"89 objectName: "homepage"
9090
91 text: i18n.tr("Homepage")91 text: i18n.tr("Homepage")
@@ -94,7 +94,7 @@
94 onClicked: PopupUtils.open(homepageDialog)94 onClicked: PopupUtils.open(homepageDialog)
95 }95 }
9696
97 ListItem.Standard {97 ListItems.Standard {
98 objectName: "restoreSession"98 objectName: "restoreSession"
9999
100 text: i18n.tr("Restore previous session at startup")100 text: i18n.tr("Restore previous session at startup")
@@ -112,7 +112,7 @@
112 }112 }
113 }113 }
114114
115 ListItem.Standard {115 ListItems.Standard {
116 objectName: "backgroundTabs"116 objectName: "backgroundTabs"
117117
118 text: i18n.tr("Allow opening new tabs in background")118 text: i18n.tr("Allow opening new tabs in background")
@@ -131,15 +131,15 @@
131 }131 }
132 }132 }
133133
134 ListItem.Standard {134 ListItems.Standard {
135 objectName: "privacy"135 objectName: "privacy"
136136
137 text: i18n.tr("Privacy")137 text: i18n.tr("Privacy & permissions")
138138
139 onClicked: privacyComponent.createObject(subpageContainer)139 onClicked: privacyComponent.createObject(subpageContainer)
140 }140 }
141141
142 ListItem.Standard {142 ListItems.Standard {
143 objectName: "reset"143 objectName: "reset"
144144
145 text: i18n.tr("Reset browser settings")145 text: i18n.tr("Reset browser settings")
@@ -186,7 +186,7 @@
186186
187 model: searchEngines.engines187 model: searchEngines.engines
188188
189 delegate: ListItem.Standard {189 delegate: ListItems.Standard {
190 objectName: "searchEngineDelegate_" + index190 objectName: "searchEngineDelegate_" + index
191 SearchEngine {191 SearchEngine {
192 id: searchEngineDelegate192 id: searchEngineDelegate
@@ -224,7 +224,7 @@
224 SettingsPageHeader {224 SettingsPageHeader {
225 id: privacyTitle225 id: privacyTitle
226 onBack: privacyItem.destroy()226 onBack: privacyItem.destroy()
227 text: i18n.tr("Privacy")227 text: i18n.tr("Privacy & permissions")
228 }228 }
229229
230 Flickable {230 Flickable {
@@ -243,7 +243,13 @@
243 id: privacyCol243 id: privacyCol
244 width: parent.width244 width: parent.width
245245
246 ListItem.Standard {246 ListItems.Standard {
247 objectName: "privacy.mediaAccess"
248 text: i18n.tr("Camera & microphone")
249 onClicked: mediaAccessComponent.createObject(subpageContainer)
250 }
251
252 ListItems.Standard {
247 objectName: "privacy.clearHistory"253 objectName: "privacy.clearHistory"
248 text: i18n.tr("Clear Browsing History")254 text: i18n.tr("Clear Browsing History")
249 enabled: HistoryModel.count > 0255 enabled: HistoryModel.count > 0
@@ -253,7 +259,7 @@
253 }259 }
254 }260 }
255261
256 ListItem.Standard {262 ListItems.Standard {
257 objectName: "privacy.clearCache"263 objectName: "privacy.clearCache"
258 text: i18n.tr("Clear Cache")264 text: i18n.tr("Clear Cache")
259 onClicked: {265 onClicked: {
@@ -359,5 +365,83 @@
359 }365 }
360 }366 }
361 }367 }
368
369 Component {
370 id: mediaAccessComponent
371
372 Item {
373 id: mediaAccessItem
374 objectName: "mediaAccessSettings"
375 anchors.fill: parent
376
377 Rectangle {
378 anchors.fill: parent
379 color: "#f6f6f6"
380 }
381
382 SettingsPageHeader {
383 id: mediaAccessTitle
384
385 onBack: mediaAccessItem.destroy()
386 text: i18n.tr("Camera & microphone")
387 }
388
389 Flickable {
390 anchors {
391 top: mediaAccessTitle.bottom
392 left: parent.left
393 right: parent.right
394 bottom: parent.bottom
395 }
396
397 clip: true
398
399 contentHeight: mediaAccessCol.height
400
401 Column {
402 id: mediaAccessCol
403 width: parent.width
404
405 ListItems.Standard {
406 text: i18n.tr("Microphone")
407 }
408
409 SettingsDeviceSelector {
410 anchors.left: parent.left
411 anchors.right: parent.right
412
413 isAudio: true
414 visible: devicesCount > 0
415 enabled: devicesCount > 1
416
417 defaultDevice: settings.defaultAudioDevice
418 onDeviceSelected: {
419 SharedWebContext.sharedContext.defaultAudioCaptureDeviceId = id
420 settings.defaultAudioDevice = id
421 }
422 }
423
424 ListItems.Standard {
425 text: i18n.tr("Camera")
426 }
427
428 SettingsDeviceSelector {
429 anchors.left: parent.left
430 anchors.right: parent.right
431
432 isAudio: false
433 visible: devicesCount > 0
434 enabled: devicesCount > 1
435
436 defaultDevice: settings.defaultVideoDevice
437 onDeviceSelected: {
438 SharedWebContext.sharedContext.defaultVideoCaptureDeviceId = id
439 settings.defaultVideoDevice = id
440 }
441 }
442 }
443 }
444 }
445 }
362}446}
363447
364448
=== modified file 'tests/autopilot/webbrowser_app/emulators/browser.py'
--- tests/autopilot/webbrowser_app/emulators/browser.py 2015-10-08 15:26:18 +0000
+++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-10-19 12:13:40 +0000
@@ -123,6 +123,9 @@
123 def get_http_auth_dialog(self):123 def get_http_auth_dialog(self):
124 return self.wait_select_single(HttpAuthenticationDialog)124 return self.wait_select_single(HttpAuthenticationDialog)
125125
126 def get_media_access_dialog(self):
127 return self.wait_select_single(MediaAccessDialog)
128
126 def get_tabs_view(self):129 def get_tabs_view(self):
127 return self.wait_select_single(TabsList, visible=True)130 return self.wait_select_single(TabsList, visible=True)
128131
@@ -402,6 +405,21 @@
402 return self.select_single("TextField", objectName="password")405 return self.select_single("TextField", objectName="password")
403406
404407
408class MediaAccessDialog(uitk.UbuntuUIToolkitCustomProxyObjectBase):
409
410 @autopilot.logging.log_action(logger.info)
411 def click_deny_button(self):
412 button = self.select_single("Button",
413 objectName="mediaAccessDialog.denyButton")
414 self.pointing_device.click_object(button)
415
416 @autopilot.logging.log_action(logger.info)
417 def click_allow_button(self):
418 button = self.select_single("Button",
419 objectName="mediaAccessDialog.allowButton")
420 self.pointing_device.click_object(button)
421
422
405class TabPreview(uitk.UbuntuUIToolkitCustomProxyObjectBase):423class TabPreview(uitk.UbuntuUIToolkitCustomProxyObjectBase):
406424
407 @autopilot.logging.log_action(logger.info)425 @autopilot.logging.log_action(logger.info)
408426
=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
--- tests/autopilot/webbrowser_app/tests/http_server.py 2015-09-15 15:21:15 +0000
+++ tests/autopilot/webbrowser_app/tests/http_server.py 2015-10-19 12:13:40 +0000
@@ -193,6 +193,16 @@
193 self.send_auth_request()193 self.send_auth_request()
194 else:194 else:
195 self.send_auth_request()195 self.send_auth_request()
196 elif self.path.startswith("/media/"):
197 self.send_response(200)
198 permissions = self.path[len("/media/"):]
199 self.send_html(
200 "<script>navigator.webkitGetUserMedia("
201 "{video: " + ("true" if "v" in permissions else "false") +
202 ", audio: " + ("true" if "a" in permissions else "false") +
203 "}, function() { location.href = '/test1' } " +
204 ", function() { location.href = '/test2' })</script>"
205 )
196 else:206 else:
197 self.send_error(404)207 self.send_error(404)
198208
199209
=== added file 'tests/autopilot/webbrowser_app/tests/test_media_access_permission.py'
--- tests/autopilot/webbrowser_app/tests/test_media_access_permission.py 1970-01-01 00:00:00 +0000
+++ tests/autopilot/webbrowser_app/tests/test_media_access_permission.py 2015-10-19 12:13:40 +0000
@@ -0,0 +1,98 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Copyright 2015 Canonical
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import testtools
18from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
19
20
21class TestMediaAccessPermission(StartOpenRemotePageTestCaseBase):
22
23 def setUp(self):
24 super(TestMediaAccessPermission, self).setUp()
25 self.url = self.base_url + "/media/"
26 self.allowed_url = self.base_url + "/test1"
27 self.denied_url = self.base_url + "/test2"
28
29 @testtools.skip("We can't guarantee/test that audio/video devices exist")
30 def test_allow(self):
31 # verify that trying to access any media raises an authorization dialog
32 url = self.url + "a"
33 self.main_window.go_to_url(url)
34 self.main_window.wait_until_page_loaded(url)
35 dialog = self.main_window.get_media_access_dialog()
36
37 # note that we have no easy way to verify that the browser actually
38 # grants or denied permission based on our choice, because we can't
39 # easily inspect the contents of the page from AP tests. the simplest
40 # workaround I could find was to redirect the user to two different
41 # pages upon permission granted or denied, and detect that instead
42 dialog.click_allow_button()
43 dialog.wait_until_destroyed()
44 self.main_window.wait_until_page_loaded(self.allowed_url)
45
46 # verify that trying to access the same media for the same origin in
47 # the same session will not ask for permission again...
48 self.main_window.go_to_url(url)
49 self.main_window.wait_until_page_loaded(self.allowed_url)
50
51 # ...but it will ask if we try to access other media
52 url = self.url + "v"
53 self.main_window.go_to_url(url)
54 self.main_window.wait_until_page_loaded(url)
55 dialog = self.main_window.get_media_access_dialog()
56 dialog.click_allow_button()
57 dialog.wait_until_destroyed()
58 self.main_window.wait_until_page_loaded(self.allowed_url)
59
60 # now that we granted both permissions, verify that asking for both
61 # together will also not raise the dialog
62 url = self.url + "av"
63 self.main_window.go_to_url(url)
64 self.main_window.wait_until_page_loaded(self.allowed_url)
65
66 def test_deny(self):
67 # verify that trying to access any media raises an authorization dialog
68 # and we get redirected to the denial page in case we refuse to give
69 # permission
70 url = self.url + "a"
71 self.main_window.go_to_url(url)
72 self.main_window.wait_until_page_loaded(url)
73 dialog = self.main_window.get_media_access_dialog()
74 dialog.click_deny_button()
75 dialog.wait_until_destroyed()
76 self.main_window.wait_until_page_loaded(self.denied_url)
77
78 # verify that trying to access the same media for the same origin in
79 # the same session will not ask for permission again...
80 self.main_window.go_to_url(url)
81 self.main_window.wait_until_page_loaded(self.denied_url)
82
83 @testtools.skip("Skipping due to oxide bug, see http://pad.lv/1501017")
84 def test_deny_combined(self):
85 # deny first one input type, then try to ask both and verify that a
86 # request is made for the media that was not asked for the fist time
87 url = self.url + "a"
88 self.main_window.go_to_url(url)
89 self.main_window.wait_until_page_loaded(url)
90 dialog = self.main_window.get_media_access_dialog()
91 dialog.click_deny_button()
92 dialog.wait_until_destroyed()
93 self.main_window.wait_until_page_loaded(self.denied_url)
94
95 url = self.url + "av"
96 self.main_window.go_to_url(url)
97 self.main_window.wait_until_page_loaded(url)
98 dialog = self.main_window.get_media_access_dialog()
099
=== added file 'tests/unittests/qml/tst_SettingsPage.qml'
--- tests/unittests/qml/tst_SettingsPage.qml 1970-01-01 00:00:00 +0000
+++ tests/unittests/qml/tst_SettingsPage.qml 2015-10-19 12:13:40 +0000
@@ -0,0 +1,113 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import QtTest 1.0
21import Ubuntu.Test 1.0
22import webbrowserapp.private 0.1
23import webbrowsertest.private 0.1
24import "../../../src/app/webbrowser"
25
26Item {
27 width: 400
28 height: 600
29
30 property var settingsPage: settingsPageLoader.item
31
32 Loader {
33 id: settingsPageLoader
34 anchors.fill: parent
35 active: false
36 sourceComponent: SettingsPage {
37 anchors.fill: parent
38
39 // NOTE: the following properties are not necessary for the tests
40 // currently in this file, but if we don't provide them a lot of
41 // warnings will be generated.
42 // Ideally either more tests that use them will be added or the code
43 // in SettingsPage will be refactored to cope with the missing
44 // settings.
45 settingsObject: QtObject {
46 property url homepage
47 property string searchEngine
48 property int newTabDefaultSection: 0
49 }
50 }
51 }
52
53 UbuntuTestCase {
54 name: "TestSettingsPage"
55 when: windowShown
56
57 function clickItem(item) {
58 var center = centerOf(item)
59 mouseClick(item, center.x, center.y)
60 }
61
62 function swipeItemRight(item) {
63 var center = centerOf(item)
64 var dx = item.width * 0.5
65 mousePress(item, center.x, center.y)
66 mouseMoveSlowly(item, center.x, center.y, dx, 0, 10, 0.01)
67 mouseRelease(item, center.x + dx, center.y)
68 }
69
70 function init() {
71 settingsPageLoader.active = true
72 waitForRendering(settingsPageLoader.item)
73 }
74
75 function cleanup() {
76 settingsPageLoader.active = false
77 }
78
79 function getListItems(name, itemName) {
80 var list = findChild(settingsPage, name)
81 var items = []
82 if (list) {
83 // ensure all the delegates are created
84 list.cacheBuffer = list.count * 1000
85
86 // In some cases the ListView might add other children to the
87 // contentItem, so we filter the list of children to include
88 // only actual delegates (names for delegates in this case
89 // follow the pattern "name_index")
90 var children = list.contentItem.children
91 for (var i = 0; i < children.length; i++) {
92 if (children[i].objectName.indexOf(itemName) == 0) {
93 items.push(children[i])
94 }
95 }
96 }
97 return items
98 }
99
100 function activateSettingsItem(itemName, pageName) {
101 var item = findChild(settingsPage, itemName)
102 clickItem(item)
103 var page = findChild(settingsPage, pageName)
104 waitForRendering(page)
105 return page
106 }
107
108 function goToMediaAccessPage() {
109 activateSettingsItem("privacy", "privacySettings")
110 return activateSettingsItem("privacy.mediaAccess", "mediaAccessSettings")
111 }
112 }
113}

Subscribers

People subscribed via source and target branches

to status/vote changes: