| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/media/media_stream_devices_controller.h" |
| |
| #include "base/metrics/histogram.h" |
| #include "base/prefs/scoped_user_pref_update.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/values.h" |
| #include "chrome/browser/content_settings/tab_specific_content_settings.h" |
| #include "chrome/browser/media/media_capture_devices_dispatcher.h" |
| #include "chrome/browser/media/media_permission.h" |
| #include "chrome/browser/media/media_stream_capture_indicator.h" |
| #include "chrome/browser/media/media_stream_device_permissions.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/content_settings/core/common/content_settings_pattern.h" |
| #include "components/pref_registry/pref_registry_syncable.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/common/media_stream_request.h" |
| #include "content/public/common/origin_util.h" |
| #include "extensions/common/constants.h" |
| #include "grit/theme_resources.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| #if defined(OS_ANDROID) |
| #include "chrome/browser/android/preferences/pref_service_bridge.h" |
| #include "content/public/browser/android/content_view_core.h" |
| #include "ui/android/window_android.h" |
| #endif // OS_ANDROID |
| |
| using content::BrowserThread; |
| |
| namespace { |
| |
| enum DevicePermissionActions { |
| kAllowHttps = 0, |
| kAllowHttp, |
| kDeny, |
| kCancel, |
| kPermissionActionsMax // Must always be last! |
| }; |
| |
| // Returns true if the given ContentSettingsType is being requested in |
| // |request|. |
| bool ContentTypeIsRequested(ContentSettingsType type, |
| const content::MediaStreamRequest& request) { |
| if (request.request_type == content::MEDIA_OPEN_DEVICE) |
| return true; |
| |
| if (type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC) |
| return request.audio_type == content::MEDIA_DEVICE_AUDIO_CAPTURE; |
| |
| if (type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA) |
| return request.video_type == content::MEDIA_DEVICE_VIDEO_CAPTURE; |
| |
| return false; |
| } |
| |
| } // namespace |
| |
| MediaStreamDevicesController::MediaStreamDevicesController( |
| content::WebContents* web_contents, |
| const content::MediaStreamRequest& request, |
| const content::MediaResponseCallback& callback) |
| : web_contents_(web_contents), |
| request_(request), |
| callback_(callback) { |
| profile_ = Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| content_settings_ = TabSpecificContentSettings::FromWebContents(web_contents); |
| |
| content::MediaStreamRequestResult denial_reason = content::MEDIA_DEVICE_OK; |
| old_audio_setting_ = GetContentSetting(CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC, |
| request_, &denial_reason); |
| old_video_setting_ = GetContentSetting( |
| CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, request_, &denial_reason); |
| |
| // If either setting is ask, we show the infobar. |
| if (old_audio_setting_ == CONTENT_SETTING_ASK || |
| old_video_setting_ == CONTENT_SETTING_ASK) { |
| return; |
| } |
| |
| // Otherwise we can run the callback immediately. |
| RunCallback(old_audio_setting_, old_video_setting_, denial_reason); |
| } |
| |
| MediaStreamDevicesController::~MediaStreamDevicesController() { |
| if (!callback_.is_null()) { |
| callback_.Run(content::MediaStreamDevices(), |
| content::MEDIA_DEVICE_FAILED_DUE_TO_SHUTDOWN, |
| scoped_ptr<content::MediaStreamUI>()); |
| } |
| } |
| |
| // static |
| void MediaStreamDevicesController::RegisterProfilePrefs( |
| user_prefs::PrefRegistrySyncable* prefs) { |
| prefs->RegisterBooleanPref(prefs::kVideoCaptureAllowed, true); |
| prefs->RegisterBooleanPref(prefs::kAudioCaptureAllowed, true); |
| prefs->RegisterListPref(prefs::kVideoCaptureAllowedUrls); |
| prefs->RegisterListPref(prefs::kAudioCaptureAllowedUrls); |
| } |
| |
| bool MediaStreamDevicesController::IsAskingForAudio() const { |
| return old_audio_setting_ == CONTENT_SETTING_ASK; |
| } |
| |
| bool MediaStreamDevicesController::IsAskingForVideo() const { |
| return old_video_setting_ == CONTENT_SETTING_ASK; |
| } |
| |
| const std::string& MediaStreamDevicesController::GetSecurityOriginSpec() const { |
| return request_.security_origin.spec(); |
| } |
| |
| int MediaStreamDevicesController::GetIconID() const { |
| if (IsAskingForVideo()) |
| return IDR_INFOBAR_MEDIA_STREAM_CAMERA; |
| |
| return IDR_INFOBAR_MEDIA_STREAM_MIC; |
| } |
| |
| base::string16 MediaStreamDevicesController::GetMessageText() const { |
| int message_id = IDS_MEDIA_CAPTURE_AUDIO_AND_VIDEO; |
| if (!IsAskingForAudio()) |
| message_id = IDS_MEDIA_CAPTURE_VIDEO_ONLY; |
| else if (!IsAskingForVideo()) |
| message_id = IDS_MEDIA_CAPTURE_AUDIO_ONLY; |
| return l10n_util::GetStringFUTF16( |
| message_id, base::UTF8ToUTF16(GetSecurityOriginSpec())); |
| } |
| |
| base::string16 MediaStreamDevicesController::GetMessageTextFragment() const { |
| int message_id = IDS_MEDIA_CAPTURE_AUDIO_AND_VIDEO_PERMISSION_FRAGMENT; |
| if (!IsAskingForAudio()) |
| message_id = IDS_MEDIA_CAPTURE_VIDEO_ONLY_PERMISSION_FRAGMENT; |
| else if (!IsAskingForVideo()) |
| message_id = IDS_MEDIA_CAPTURE_AUDIO_ONLY_PERMISSION_FRAGMENT; |
| return l10n_util::GetStringUTF16(message_id); |
| } |
| |
| bool MediaStreamDevicesController::HasUserGesture() const { |
| return request_.user_gesture; |
| } |
| |
| GURL MediaStreamDevicesController::GetRequestingHostname() const { |
| return request_.security_origin; |
| } |
| |
| void MediaStreamDevicesController::PermissionGranted() { |
| GURL origin(GetSecurityOriginSpec()); |
| if (content::IsOriginSecure(origin)) { |
| UMA_HISTOGRAM_ENUMERATION("Media.DevicePermissionActions", |
| kAllowHttps, kPermissionActionsMax); |
| } else { |
| UMA_HISTOGRAM_ENUMERATION("Media.DevicePermissionActions", |
| kAllowHttp, kPermissionActionsMax); |
| } |
| RunCallback(GetNewSetting(CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC, |
| old_audio_setting_, CONTENT_SETTING_ALLOW), |
| GetNewSetting(CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, |
| old_video_setting_, CONTENT_SETTING_ALLOW), |
| content::MEDIA_DEVICE_PERMISSION_DENIED); |
| } |
| |
| void MediaStreamDevicesController::PermissionDenied() { |
| UMA_HISTOGRAM_ENUMERATION("Media.DevicePermissionActions", |
| kDeny, kPermissionActionsMax); |
| RunCallback(GetNewSetting(CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC, |
| old_audio_setting_, CONTENT_SETTING_BLOCK), |
| GetNewSetting(CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, |
| old_video_setting_, CONTENT_SETTING_BLOCK), |
| content::MEDIA_DEVICE_PERMISSION_DENIED); |
| } |
| |
| void MediaStreamDevicesController::Cancelled() { |
| UMA_HISTOGRAM_ENUMERATION("Media.DevicePermissionActions", |
| kCancel, kPermissionActionsMax); |
| RunCallback(old_audio_setting_, old_video_setting_, |
| content::MEDIA_DEVICE_PERMISSION_DISMISSED); |
| } |
| |
| void MediaStreamDevicesController::RequestFinished() { |
| delete this; |
| } |
| |
| content::MediaStreamDevices MediaStreamDevicesController::GetDevices( |
| ContentSetting audio_setting, |
| ContentSetting video_setting) { |
| bool audio_allowed = audio_setting == CONTENT_SETTING_ALLOW; |
| bool video_allowed = video_setting == CONTENT_SETTING_ALLOW; |
| |
| if (!audio_allowed && !video_allowed) |
| return content::MediaStreamDevices(); |
| |
| content::MediaStreamDevices devices; |
| switch (request_.request_type) { |
| case content::MEDIA_OPEN_DEVICE: { |
| const content::MediaStreamDevice* device = NULL; |
| // For open device request, when requested device_id is empty, pick |
| // the first available of the given type. If requested device_id is |
| // not empty, return the desired device if it's available. Otherwise, |
| // return no device. |
| if (audio_allowed && |
| request_.audio_type == content::MEDIA_DEVICE_AUDIO_CAPTURE) { |
| DCHECK_EQ(content::MEDIA_NO_SERVICE, request_.video_type); |
| if (!request_.requested_audio_device_id.empty()) { |
| device = |
| MediaCaptureDevicesDispatcher::GetInstance() |
| ->GetRequestedAudioDevice(request_.requested_audio_device_id); |
| } else { |
| device = MediaCaptureDevicesDispatcher::GetInstance() |
| ->GetFirstAvailableAudioDevice(); |
| } |
| } else if (video_allowed && |
| request_.video_type == content::MEDIA_DEVICE_VIDEO_CAPTURE) { |
| DCHECK_EQ(content::MEDIA_NO_SERVICE, request_.audio_type); |
| // Pepper API opens only one device at a time. |
| if (!request_.requested_video_device_id.empty()) { |
| device = |
| MediaCaptureDevicesDispatcher::GetInstance() |
| ->GetRequestedVideoDevice(request_.requested_video_device_id); |
| } else { |
| device = MediaCaptureDevicesDispatcher::GetInstance() |
| ->GetFirstAvailableVideoDevice(); |
| } |
| } |
| if (device) |
| devices.push_back(*device); |
| break; |
| } |
| case content::MEDIA_GENERATE_STREAM: { |
| bool get_default_audio_device = audio_allowed; |
| bool get_default_video_device = video_allowed; |
| |
| // Get the exact audio or video device if an id is specified. |
| if (audio_allowed && !request_.requested_audio_device_id.empty()) { |
| const content::MediaStreamDevice* audio_device = |
| MediaCaptureDevicesDispatcher::GetInstance() |
| ->GetRequestedAudioDevice(request_.requested_audio_device_id); |
| if (audio_device) { |
| devices.push_back(*audio_device); |
| get_default_audio_device = false; |
| } |
| } |
| if (video_allowed && !request_.requested_video_device_id.empty()) { |
| const content::MediaStreamDevice* video_device = |
| MediaCaptureDevicesDispatcher::GetInstance() |
| ->GetRequestedVideoDevice(request_.requested_video_device_id); |
| if (video_device) { |
| devices.push_back(*video_device); |
| get_default_video_device = false; |
| } |
| } |
| |
| // If either or both audio and video devices were requested but not |
| // specified by id, get the default devices. |
| if (get_default_audio_device || get_default_video_device) { |
| MediaCaptureDevicesDispatcher::GetInstance() |
| ->GetDefaultDevicesForProfile(profile_, get_default_audio_device, |
| get_default_video_device, &devices); |
| } |
| break; |
| } |
| case content::MEDIA_DEVICE_ACCESS: { |
| // Get the default devices for the request. |
| MediaCaptureDevicesDispatcher::GetInstance()->GetDefaultDevicesForProfile( |
| profile_, audio_allowed, video_allowed, &devices); |
| break; |
| } |
| case content::MEDIA_ENUMERATE_DEVICES: { |
| // Do nothing. |
| NOTREACHED(); |
| break; |
| } |
| } // switch |
| |
| if (audio_allowed) { |
| profile_->GetHostContentSettingsMap()->UpdateLastUsageByPattern( |
| ContentSettingsPattern::FromURLNoWildcard(request_.security_origin), |
| ContentSettingsPattern::Wildcard(), |
| CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC); |
| } |
| if (video_allowed) { |
| profile_->GetHostContentSettingsMap()->UpdateLastUsageByPattern( |
| ContentSettingsPattern::FromURLNoWildcard(request_.security_origin), |
| ContentSettingsPattern::Wildcard(), |
| CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); |
| } |
| |
| return devices; |
| } |
| |
| void MediaStreamDevicesController::RunCallback( |
| ContentSetting audio_setting, |
| ContentSetting video_setting, |
| content::MediaStreamRequestResult denial_reason) { |
| StorePermission(audio_setting, video_setting); |
| UpdateTabSpecificContentSettings(audio_setting, video_setting); |
| |
| content::MediaStreamDevices devices = |
| GetDevices(audio_setting, video_setting); |
| |
| // If either audio or video are allowed then the callback should report |
| // success, otherwise we report |denial_reason|. |
| content::MediaStreamRequestResult request_result = content::MEDIA_DEVICE_OK; |
| if (audio_setting != CONTENT_SETTING_ALLOW && |
| video_setting != CONTENT_SETTING_ALLOW) { |
| DCHECK_NE(content::MEDIA_DEVICE_OK, denial_reason); |
| request_result = denial_reason; |
| } else if (devices.empty()) { |
| // Even if one of the content settings was allowed, if there are no devices |
| // at this point we still report a failure. |
| request_result = content::MEDIA_DEVICE_NO_HARDWARE; |
| } |
| |
| scoped_ptr<content::MediaStreamUI> ui; |
| if (!devices.empty()) { |
| ui = MediaCaptureDevicesDispatcher::GetInstance() |
| ->GetMediaStreamCaptureIndicator() |
| ->RegisterMediaStream(web_contents_, devices); |
| } |
| content::MediaResponseCallback cb = callback_; |
| callback_.Reset(); |
| cb.Run(devices, request_result, ui.Pass()); |
| } |
| |
| void MediaStreamDevicesController::StorePermission( |
| ContentSetting new_audio_setting, |
| ContentSetting new_video_setting) const { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| ContentSettingsPattern primary_pattern = |
| ContentSettingsPattern::FromURLNoWildcard(request_.security_origin); |
| |
| if (IsAskingForAudio() && new_audio_setting != CONTENT_SETTING_ASK) { |
| if (ShouldPersistContentSetting(new_audio_setting, request_.security_origin, |
| request_.request_type)) { |
| profile_->GetHostContentSettingsMap()->SetContentSetting( |
| primary_pattern, ContentSettingsPattern::Wildcard(), |
| CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC, std::string(), |
| new_audio_setting); |
| } |
| } |
| if (IsAskingForVideo() && new_video_setting != CONTENT_SETTING_ASK) { |
| if (ShouldPersistContentSetting(new_video_setting, request_.security_origin, |
| request_.request_type)) { |
| profile_->GetHostContentSettingsMap()->SetContentSetting( |
| primary_pattern, ContentSettingsPattern::Wildcard(), |
| CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, std::string(), |
| new_video_setting); |
| } |
| } |
| } |
| |
| void MediaStreamDevicesController::UpdateTabSpecificContentSettings( |
| ContentSetting audio_setting, |
| ContentSetting video_setting) const { |
| if (!content_settings_) |
| return; |
| |
| TabSpecificContentSettings::MicrophoneCameraState microphone_camera_state = |
| TabSpecificContentSettings::MICROPHONE_CAMERA_NOT_ACCESSED; |
| std::string selected_audio_device; |
| std::string selected_video_device; |
| std::string requested_audio_device = request_.requested_audio_device_id; |
| std::string requested_video_device = request_.requested_video_device_id; |
| |
| // TODO(raymes): Why do we use the defaults here for the selected devices? |
| // Shouldn't we just use the devices that were actually selected? |
| PrefService* prefs = Profile::FromBrowserContext( |
| web_contents_->GetBrowserContext())->GetPrefs(); |
| if (audio_setting != CONTENT_SETTING_DEFAULT) { |
| selected_audio_device = |
| requested_audio_device.empty() |
| ? prefs->GetString(prefs::kDefaultAudioCaptureDevice) |
| : requested_audio_device; |
| microphone_camera_state |= |
| TabSpecificContentSettings::MICROPHONE_ACCESSED | |
| (audio_setting == CONTENT_SETTING_ALLOW |
| ? 0 |
| : TabSpecificContentSettings::MICROPHONE_BLOCKED); |
| } |
| |
| if (video_setting != CONTENT_SETTING_DEFAULT) { |
| selected_video_device = |
| requested_video_device.empty() |
| ? prefs->GetString(prefs::kDefaultVideoCaptureDevice) |
| : requested_video_device; |
| microphone_camera_state |= |
| TabSpecificContentSettings::CAMERA_ACCESSED | |
| (video_setting == CONTENT_SETTING_ALLOW |
| ? 0 |
| : TabSpecificContentSettings::CAMERA_BLOCKED); |
| } |
| |
| content_settings_->OnMediaStreamPermissionSet( |
| request_.security_origin, microphone_camera_state, selected_audio_device, |
| selected_video_device, requested_audio_device, requested_video_device); |
| } |
| |
| ContentSetting MediaStreamDevicesController::GetContentSetting( |
| ContentSettingsType content_type, |
| const content::MediaStreamRequest& request, |
| content::MediaStreamRequestResult* denial_reason) const { |
| DCHECK(content_type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC || |
| content_type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); |
| |
| std::string requested_device_id; |
| if (content_type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC) |
| requested_device_id = request.requested_audio_device_id; |
| else if (content_type == CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA) |
| requested_device_id = request.requested_video_device_id; |
| |
| if (!IsUserAcceptAllowed(content_type)) { |
| *denial_reason = content::MEDIA_DEVICE_PERMISSION_DENIED; |
| return CONTENT_SETTING_BLOCK; |
| } |
| |
| if (ContentTypeIsRequested(content_type, request)) { |
| MediaPermission permission(content_type, request.request_type, |
| request.security_origin, requested_device_id, |
| profile_); |
| return permission.GetPermissionStatus(denial_reason); |
| } |
| // Return the default content setting if the device is not requested. |
| return CONTENT_SETTING_DEFAULT; |
| } |
| |
| ContentSetting MediaStreamDevicesController::GetNewSetting( |
| ContentSettingsType content_type, |
| ContentSetting old_setting, |
| ContentSetting user_decision) const { |
| DCHECK(user_decision == CONTENT_SETTING_ALLOW || |
| user_decision == CONTENT_SETTING_BLOCK); |
| ContentSetting result = old_setting; |
| if (old_setting == CONTENT_SETTING_ASK) { |
| if (user_decision == CONTENT_SETTING_ALLOW && |
| IsUserAcceptAllowed(content_type)) { |
| result = CONTENT_SETTING_ALLOW; |
| } else if (user_decision == CONTENT_SETTING_BLOCK) { |
| result = CONTENT_SETTING_BLOCK; |
| } |
| } |
| return result; |
| } |
| |
| bool MediaStreamDevicesController::IsUserAcceptAllowed( |
| ContentSettingsType content_type) const { |
| #if defined(OS_ANDROID) |
| content::ContentViewCore* cvc = |
| content::ContentViewCore::FromWebContents(web_contents_); |
| if (!cvc) |
| return false; |
| |
| ui::WindowAndroid* window_android = cvc->GetWindowAndroid(); |
| if (!window_android) |
| return false; |
| |
| std::string android_permission = |
| PrefServiceBridge::GetAndroidPermissionForContentSetting(content_type); |
| bool android_permission_blocked = false; |
| if (!android_permission.empty()) { |
| android_permission_blocked = |
| !window_android->HasPermission(android_permission) && |
| !window_android->CanRequestPermission(android_permission); |
| } |
| if (android_permission_blocked) |
| return false; |
| |
| // Don't approve device requests if the tab was hidden. |
| // TODO(qinmin): Add a test for this. http://crbug.com/396869. |
| // TODO(raymes): Shouldn't this apply to all permissions not just audio/video? |
| return web_contents_->GetRenderWidgetHostView()->IsShowing(); |
| #endif |
| return true; |
| } |