Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / media / media_stream_capture_indicator.cc
blob794eb968698a5ab0124fd97ba10f4e572773a2f4
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/media/media_stream_capture_indicator.h"
7 #include "base/bind.h"
8 #include "base/i18n/rtl.h"
9 #include "base/logging.h"
10 #include "base/memory/scoped_ptr.h"
11 #include "base/prefs/pref_service.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/app/chrome_command_ids.h"
14 #include "chrome/browser/browser_process.h"
15 #include "chrome/browser/extensions/extension_service.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/status_icons/status_icon.h"
18 #include "chrome/browser/status_icons/status_tray.h"
19 #include "chrome/browser/tab_contents/tab_util.h"
20 #include "chrome/common/pref_names.h"
21 #include "content/public/browser/browser_thread.h"
22 #include "content/public/browser/content_browser_client.h"
23 #include "content/public/browser/invalidate_type.h"
24 #include "content/public/browser/web_contents.h"
25 #include "content/public/browser/web_contents_delegate.h"
26 #include "content/public/browser/web_contents_observer.h"
27 #include "extensions/common/extension.h"
28 #include "grit/chromium_strings.h"
29 #include "grit/generated_resources.h"
30 #include "grit/theme_resources.h"
31 #include "net/base/net_util.h"
32 #include "ui/base/l10n/l10n_util.h"
33 #include "ui/base/resource/resource_bundle.h"
34 #include "ui/gfx/image/image_skia.h"
36 using content::BrowserThread;
37 using content::WebContents;
39 namespace {
41 const extensions::Extension* GetExtension(WebContents* web_contents) {
42 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
44 if (!web_contents)
45 return NULL;
47 Profile* profile =
48 Profile::FromBrowserContext(web_contents->GetBrowserContext());
49 if (!profile)
50 return NULL;
52 ExtensionService* extension_service = profile->GetExtensionService();
53 if (!extension_service)
54 return NULL;
56 return extension_service->extensions()->GetExtensionOrAppByURL(
57 web_contents->GetURL());
60 // Gets the security originator of the tab. It returns a string with no '/'
61 // at the end to display in the UI.
62 base::string16 GetSecurityOrigin(WebContents* web_contents) {
63 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
65 if (!web_contents)
66 return base::string16();
68 std::string security_origin = web_contents->GetURL().GetOrigin().spec();
70 // Remove the last character if it is a '/'.
71 if (!security_origin.empty()) {
72 std::string::iterator it = security_origin.end() - 1;
73 if (*it == '/')
74 security_origin.erase(it);
77 return base::UTF8ToUTF16(security_origin);
80 base::string16 GetTitle(WebContents* web_contents) {
81 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
83 if (!web_contents)
84 return base::string16();
86 const extensions::Extension* const extension = GetExtension(web_contents);
87 if (extension)
88 return base::UTF8ToUTF16(extension->name());
90 base::string16 tab_title = web_contents->GetTitle();
92 if (tab_title.empty()) {
93 // If the page's title is empty use its security originator.
94 tab_title = GetSecurityOrigin(web_contents);
95 } else {
96 // If the page's title matches its URL, use its security originator.
97 Profile* profile =
98 Profile::FromBrowserContext(web_contents->GetBrowserContext());
99 std::string languages =
100 profile->GetPrefs()->GetString(prefs::kAcceptLanguages);
101 if (tab_title == net::FormatUrl(web_contents->GetURL(), languages))
102 tab_title = GetSecurityOrigin(web_contents);
105 return tab_title;
108 } // namespace
110 // Stores usage counts for all the capture devices associated with a single
111 // WebContents instance. Instances of this class are owned by
112 // MediaStreamCaptureIndicator. They also observe for the destruction of the
113 // WebContents instances and delete themselves when corresponding WebContents is
114 // deleted.
115 class MediaStreamCaptureIndicator::WebContentsDeviceUsage
116 : public content::WebContentsObserver {
117 public:
118 explicit WebContentsDeviceUsage(
119 scoped_refptr<MediaStreamCaptureIndicator> indicator,
120 WebContents* web_contents)
121 : WebContentsObserver(web_contents),
122 indicator_(indicator),
123 audio_ref_count_(0),
124 video_ref_count_(0),
125 mirroring_ref_count_(0),
126 weak_factory_(this) {
129 bool IsCapturingAudio() const { return audio_ref_count_ > 0; }
130 bool IsCapturingVideo() const { return video_ref_count_ > 0; }
131 bool IsMirroring() const { return mirroring_ref_count_ > 0; }
133 scoped_ptr<content::MediaStreamUI> RegisterMediaStream(
134 const content::MediaStreamDevices& devices);
136 // Increment ref-counts up based on the type of each device provided.
137 void AddDevices(const content::MediaStreamDevices& devices);
139 // Decrement ref-counts up based on the type of each device provided.
140 void RemoveDevices(const content::MediaStreamDevices& devices);
142 private:
143 // content::WebContentsObserver overrides.
144 virtual void WebContentsDestroyed(WebContents* web_contents) OVERRIDE {
145 indicator_->UnregisterWebContents(web_contents);
146 delete this;
149 scoped_refptr<MediaStreamCaptureIndicator> indicator_;
150 int audio_ref_count_;
151 int video_ref_count_;
152 int mirroring_ref_count_;
154 base::WeakPtrFactory<WebContentsDeviceUsage> weak_factory_;
156 DISALLOW_COPY_AND_ASSIGN(WebContentsDeviceUsage);
159 // Implements MediaStreamUI interface. Instances of this class are created for
160 // each MediaStream and their ownership is passed to MediaStream implementation
161 // in the content layer. Each UIDelegate keeps a weak pointer to the
162 // corresponding WebContentsDeviceUsage object to deliver updates about state of
163 // the stream.
164 class MediaStreamCaptureIndicator::UIDelegate
165 : public content::MediaStreamUI {
166 public:
167 UIDelegate(base::WeakPtr<WebContentsDeviceUsage> device_usage,
168 const content::MediaStreamDevices& devices)
169 : device_usage_(device_usage),
170 devices_(devices),
171 started_(false) {
172 DCHECK(!devices_.empty());
175 virtual ~UIDelegate() {
176 if (started_ && device_usage_.get())
177 device_usage_->RemoveDevices(devices_);
180 private:
181 // content::MediaStreamUI interface.
182 virtual void OnStarted(const base::Closure& close_callback) OVERRIDE {
183 DCHECK(!started_);
184 started_ = true;
185 if (device_usage_.get())
186 device_usage_->AddDevices(devices_);
189 base::WeakPtr<WebContentsDeviceUsage> device_usage_;
190 content::MediaStreamDevices devices_;
191 bool started_;
193 DISALLOW_COPY_AND_ASSIGN(UIDelegate);
197 scoped_ptr<content::MediaStreamUI>
198 MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream(
199 const content::MediaStreamDevices& devices) {
200 return scoped_ptr<content::MediaStreamUI>(new UIDelegate(
201 weak_factory_.GetWeakPtr(), devices));
204 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices(
205 const content::MediaStreamDevices& devices) {
206 for (content::MediaStreamDevices::const_iterator it = devices.begin();
207 it != devices.end(); ++it) {
208 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
209 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
210 ++mirroring_ref_count_;
211 } else if (content::IsAudioMediaType(it->type)) {
212 ++audio_ref_count_;
213 } else if (content::IsVideoMediaType(it->type)) {
214 ++video_ref_count_;
215 } else {
216 NOTIMPLEMENTED();
220 if (web_contents())
221 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
223 indicator_->UpdateNotificationUserInterface();
226 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices(
227 const content::MediaStreamDevices& devices) {
228 for (content::MediaStreamDevices::const_iterator it = devices.begin();
229 it != devices.end(); ++it) {
230 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
231 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
232 --mirroring_ref_count_;
233 } else if (content::IsAudioMediaType(it->type)) {
234 --audio_ref_count_;
235 } else if (content::IsVideoMediaType(it->type)) {
236 --video_ref_count_;
237 } else {
238 NOTIMPLEMENTED();
242 DCHECK_GE(audio_ref_count_, 0);
243 DCHECK_GE(video_ref_count_, 0);
244 DCHECK_GE(mirroring_ref_count_, 0);
246 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
247 indicator_->UpdateNotificationUserInterface();
250 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
251 : status_icon_(NULL),
252 mic_image_(NULL),
253 camera_image_(NULL) {
256 MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() {
257 // The user is responsible for cleaning up by reporting the closure of any
258 // opened devices. However, there exists a race condition at shutdown: The UI
259 // thread may be stopped before CaptureDevicesClosed() posts the task to
260 // invoke DoDevicesClosedOnUIThread(). In this case, usage_map_ won't be
261 // empty like it should.
262 DCHECK(usage_map_.empty() ||
263 !BrowserThread::IsMessageLoopValid(BrowserThread::UI));
265 // Free any WebContentsDeviceUsage objects left over.
266 for (UsageMap::const_iterator it = usage_map_.begin(); it != usage_map_.end();
267 ++it) {
268 delete it->second;
272 scoped_ptr<content::MediaStreamUI>
273 MediaStreamCaptureIndicator::RegisterMediaStream(
274 content::WebContents* web_contents,
275 const content::MediaStreamDevices& devices) {
276 WebContentsDeviceUsage*& usage = usage_map_[web_contents];
277 if (!usage)
278 usage = new WebContentsDeviceUsage(this, web_contents);
279 return usage->RegisterMediaStream(devices);
282 void MediaStreamCaptureIndicator::ExecuteCommand(int command_id,
283 int event_flags) {
284 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
286 const int index =
287 command_id - IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
288 DCHECK_LE(0, index);
289 DCHECK_GT(static_cast<int>(command_targets_.size()), index);
290 WebContents* const web_contents = command_targets_[index];
291 UsageMap::const_iterator it = usage_map_.find(web_contents);
292 if (it == usage_map_.end())
293 return;
294 web_contents->GetDelegate()->ActivateContents(web_contents);
297 bool MediaStreamCaptureIndicator::IsCapturingUserMedia(
298 content::WebContents* web_contents) const {
299 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
301 UsageMap::const_iterator it = usage_map_.find(web_contents);
302 return (it != usage_map_.end() &&
303 (it->second->IsCapturingAudio() || it->second->IsCapturingVideo()));
306 bool MediaStreamCaptureIndicator::IsCapturingVideo(
307 content::WebContents* web_contents) const {
308 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
310 UsageMap::const_iterator it = usage_map_.find(web_contents);
311 return (it != usage_map_.end() && it->second->IsCapturingVideo());
314 bool MediaStreamCaptureIndicator::IsCapturingAudio(
315 content::WebContents* web_contents) const {
316 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
318 UsageMap::const_iterator it = usage_map_.find(web_contents);
319 return (it != usage_map_.end() && it->second->IsCapturingAudio());
322 bool MediaStreamCaptureIndicator::IsBeingMirrored(
323 content::WebContents* web_contents) const {
324 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
326 UsageMap::const_iterator it = usage_map_.find(web_contents);
327 return it != usage_map_.end() && it->second->IsMirroring();
330 void MediaStreamCaptureIndicator::UnregisterWebContents(
331 WebContents* web_contents) {
332 usage_map_.erase(web_contents);
333 UpdateNotificationUserInterface();
336 void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon(bool audio,
337 bool video) {
338 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
339 if (status_icon_)
340 return;
342 // If there is no browser process, we should not create the status tray.
343 if (!g_browser_process)
344 return;
346 StatusTray* status_tray = g_browser_process->status_tray();
347 if (!status_tray)
348 return;
350 EnsureStatusTrayIconResources();
352 gfx::ImageSkia image;
353 base::string16 tool_tip;
354 GetStatusTrayIconInfo(audio, video, &image, &tool_tip);
355 DCHECK(!image.isNull());
356 DCHECK(!tool_tip.empty());
358 status_icon_ = status_tray->CreateStatusIcon(
359 StatusTray::MEDIA_STREAM_CAPTURE_ICON, image, tool_tip);
362 void MediaStreamCaptureIndicator::EnsureStatusTrayIconResources() {
363 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
364 if (!mic_image_) {
365 mic_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
366 IDR_INFOBAR_MEDIA_STREAM_MIC);
368 if (!camera_image_) {
369 camera_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
370 IDR_INFOBAR_MEDIA_STREAM_CAMERA);
372 DCHECK(mic_image_);
373 DCHECK(camera_image_);
376 void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() {
377 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
379 if (!status_icon_)
380 return;
382 // If there is no browser process, we should not do anything.
383 if (!g_browser_process)
384 return;
386 StatusTray* status_tray = g_browser_process->status_tray();
387 if (status_tray != NULL) {
388 status_tray->RemoveStatusIcon(status_icon_);
389 status_icon_ = NULL;
393 void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() {
394 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
395 scoped_ptr<StatusIconMenuModel> menu(new StatusIconMenuModel(this));
397 bool audio = false;
398 bool video = false;
399 int command_id = IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
400 command_targets_.clear();
402 for (UsageMap::const_iterator iter = usage_map_.begin();
403 iter != usage_map_.end(); ++iter) {
404 // Check if any audio and video devices have been used.
405 const WebContentsDeviceUsage& usage = *iter->second;
406 WebContents* const web_contents = iter->first;
408 // Audio/video icon is shown only for extensions or on Android.
409 // For regular tabs on desktop, we show an indicator in the tab icon.
410 if ((usage.IsCapturingAudio() || usage.IsCapturingVideo())
411 #if !defined(OS_ANDROID)
412 && GetExtension(web_contents)
413 #endif
415 audio = audio || usage.IsCapturingAudio();
416 video = video || usage.IsCapturingVideo();
418 command_targets_.push_back(web_contents);
419 menu->AddItem(command_id, GetTitle(web_contents));
421 // If the menu item is not a label, enable it.
422 menu->SetCommandIdEnabled(command_id,
423 command_id != IDC_MinimumLabelValue);
425 // If reaching the maximum number, no more item will be added to the menu.
426 if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST)
427 break;
428 ++command_id;
432 if (command_targets_.empty()) {
433 MaybeDestroyStatusTrayIcon();
434 return;
437 // The icon will take the ownership of the passed context menu.
438 MaybeCreateStatusTrayIcon(audio, video);
439 if (status_icon_) {
440 status_icon_->SetContextMenu(menu.Pass());
444 void MediaStreamCaptureIndicator::GetStatusTrayIconInfo(
445 bool audio,
446 bool video,
447 gfx::ImageSkia* image,
448 base::string16* tool_tip) {
449 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
450 DCHECK(audio || video);
452 int message_id = 0;
453 if (audio && video) {
454 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO;
455 *image = *camera_image_;
456 } else if (audio && !video) {
457 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY;
458 *image = *mic_image_;
459 } else if (!audio && video) {
460 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY;
461 *image = *camera_image_;
464 *tool_tip = l10n_util::GetStringUTF16(message_id);