Fix build break
[chromium-blink-merge.git] / chrome / browser / media / media_stream_capture_indicator.cc
blobfe582577b7cd8ada7c3bd0bce0c7e2e5e98ea57a
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/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/extensions/image_loader.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/status_icons/status_icon.h"
19 #include "chrome/browser/status_icons/status_tray.h"
20 #include "chrome/browser/tab_contents/tab_util.h"
21 #include "chrome/browser/ui/screen_capture_notification_ui.h"
22 #include "chrome/common/extensions/extension.h"
23 #include "chrome/common/extensions/manifest_handlers/icons_handler.h"
24 #include "chrome/common/pref_names.h"
25 #include "content/public/browser/browser_thread.h"
26 #include "content/public/browser/content_browser_client.h"
27 #include "content/public/browser/invalidate_type.h"
28 #include "content/public/browser/web_contents.h"
29 #include "content/public/browser/web_contents_delegate.h"
30 #include "content/public/browser/web_contents_observer.h"
31 #include "grit/chromium_strings.h"
32 #include "grit/generated_resources.h"
33 #include "grit/theme_resources.h"
34 #include "net/base/net_util.h"
35 #include "ui/base/l10n/l10n_util.h"
36 #include "ui/base/resource/resource_bundle.h"
37 #include "ui/gfx/image/image_skia.h"
39 using content::BrowserThread;
40 using content::WebContents;
42 namespace {
44 const extensions::Extension* GetExtension(WebContents* web_contents) {
45 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
47 if (!web_contents)
48 return NULL;
50 Profile* profile =
51 Profile::FromBrowserContext(web_contents->GetBrowserContext());
52 if (!profile)
53 return NULL;
55 ExtensionService* extension_service = profile->GetExtensionService();
56 if (!extension_service)
57 return NULL;
59 return extension_service->extensions()->GetExtensionOrAppByURL(
60 ExtensionURLInfo(web_contents->GetURL()));
63 // Gets the security originator of the tab. It returns a string with no '/'
64 // at the end to display in the UI.
65 string16 GetSecurityOrigin(WebContents* web_contents) {
66 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
68 if (!web_contents)
69 return string16();
71 std::string security_origin = web_contents->GetURL().GetOrigin().spec();
73 // Remove the last character if it is a '/'.
74 if (!security_origin.empty()) {
75 std::string::iterator it = security_origin.end() - 1;
76 if (*it == '/')
77 security_origin.erase(it);
80 return UTF8ToUTF16(security_origin);
83 string16 GetTitle(WebContents* web_contents) {
84 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
86 if (!web_contents)
87 return string16();
89 const extensions::Extension* const extension = GetExtension(web_contents);
90 if (extension)
91 return UTF8ToUTF16(extension->name());
93 string16 tab_title = web_contents->GetTitle();
95 if (tab_title.empty()) {
96 // If the page's title is empty use its security originator.
97 tab_title = GetSecurityOrigin(web_contents);
98 } else {
99 // If the page's title matches its URL, use its security originator.
100 Profile* profile =
101 Profile::FromBrowserContext(web_contents->GetBrowserContext());
102 std::string languages =
103 profile->GetPrefs()->GetString(prefs::kAcceptLanguages);
104 if (tab_title == net::FormatUrl(web_contents->GetURL(), languages))
105 tab_title = GetSecurityOrigin(web_contents);
108 return tab_title;
111 } // namespace
113 // Stores usage counts for all the capture devices associated with a single
114 // WebContents instance, and observes for the destruction of the WebContents
115 // instance.
116 class MediaStreamCaptureIndicator::WebContentsDeviceUsage
117 : protected content::WebContentsObserver {
118 public:
119 explicit WebContentsDeviceUsage(WebContents* web_contents);
121 bool IsWebContentsDestroyed() const { return web_contents() == NULL; }
123 bool IsCapturingAudio() const { return audio_ref_count_ > 0; }
124 bool IsCapturingVideo() const { return video_ref_count_ > 0; }
125 bool IsMirroring() const { return mirroring_ref_count_ > 0; }
126 bool IsCapturingScreen() const { return screen_capture_ref_count_ > 0; }
127 const base::Closure& StopScreenCaptureCallback() const {
128 return stop_screen_capture_callback_;
131 // Increment ref-counts up based on the type of each device provided. The
132 // return value is the message ID for the balloon body to show, or zero if the
133 // balloon should not be shown.
134 int AddDevices(const content::MediaStreamDevices& devices,
135 const base::Closure& close_callback);
137 // Decrement ref-counts up based on the type of each device provided.
138 void RemoveDevices(const content::MediaStreamDevices& devices);
140 private:
141 int audio_ref_count_;
142 int video_ref_count_;
143 int mirroring_ref_count_;
144 int screen_capture_ref_count_;
145 base::Closure stop_screen_capture_callback_;
147 DISALLOW_COPY_AND_ASSIGN(WebContentsDeviceUsage);
150 MediaStreamCaptureIndicator::WebContentsDeviceUsage::WebContentsDeviceUsage(
151 WebContents* web_contents)
152 : content::WebContentsObserver(web_contents),
153 audio_ref_count_(0),
154 video_ref_count_(0),
155 mirroring_ref_count_(0),
156 screen_capture_ref_count_(0) {
159 int MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices(
160 const content::MediaStreamDevices& devices,
161 const base::Closure& close_callback) {
162 bool incremented_audio_count = false;
163 bool incremented_video_count = false;
164 for (content::MediaStreamDevices::const_iterator it = devices.begin();
165 it != devices.end(); ++it) {
166 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
167 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
168 ++mirroring_ref_count_;
169 } else if (it->type == content::MEDIA_SCREEN_VIDEO_CAPTURE) {
170 ++screen_capture_ref_count_;
171 stop_screen_capture_callback_ = close_callback;
172 } else if (content::IsAudioMediaType(it->type)) {
173 ++audio_ref_count_;
174 } else if (content::IsVideoMediaType(it->type)) {
175 ++video_ref_count_;
176 } else {
177 NOTIMPLEMENTED();
181 if (incremented_audio_count && incremented_video_count)
182 return IDS_MEDIA_STREAM_STATUS_TRAY_BALLOON_BODY_AUDIO_AND_VIDEO;
183 else if (incremented_audio_count)
184 return IDS_MEDIA_STREAM_STATUS_TRAY_BALLOON_BODY_AUDIO_ONLY;
185 else if (incremented_video_count)
186 return IDS_MEDIA_STREAM_STATUS_TRAY_BALLOON_BODY_VIDEO_ONLY;
187 else
188 return 0;
191 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices(
192 const content::MediaStreamDevices& devices) {
193 for (content::MediaStreamDevices::const_iterator it = devices.begin();
194 it != devices.end(); ++it) {
195 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
196 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
197 --mirroring_ref_count_;
198 } else if (it->type == content::MEDIA_SCREEN_VIDEO_CAPTURE) {
199 --screen_capture_ref_count_;
200 } else if (content::IsAudioMediaType(it->type)) {
201 --audio_ref_count_;
202 } else if (content::IsVideoMediaType(it->type)) {
203 --video_ref_count_;
204 } else {
205 NOTIMPLEMENTED();
209 DCHECK_GE(audio_ref_count_, 0);
210 DCHECK_GE(video_ref_count_, 0);
211 DCHECK_GE(mirroring_ref_count_, 0);
212 DCHECK_GE(screen_capture_ref_count_, 0);
215 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
216 : status_icon_(NULL),
217 mic_image_(NULL),
218 camera_image_(NULL),
219 balloon_image_(NULL),
220 should_show_balloon_(false) {
223 MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() {
224 // The user is responsible for cleaning up by reporting the closure of any
225 // opened devices. However, there exists a race condition at shutdown: The UI
226 // thread may be stopped before CaptureDevicesClosed() posts the task to
227 // invoke DoDevicesClosedOnUIThread(). In this case, usage_map_ won't be
228 // empty like it should.
229 DCHECK(usage_map_.empty() ||
230 !BrowserThread::IsMessageLoopValid(BrowserThread::UI));
232 // Free any WebContentsDeviceUsage objects left over.
233 for (UsageMap::const_iterator it = usage_map_.begin(); it != usage_map_.end();
234 ++it) {
235 delete it->second;
239 bool MediaStreamCaptureIndicator::IsCommandIdChecked(
240 int command_id) const {
241 NOTIMPLEMENTED() << "There are no checked items in the MediaStream menu.";
242 return false;
245 bool MediaStreamCaptureIndicator::IsCommandIdEnabled(
246 int command_id) const {
247 return command_id != IDC_MinimumLabelValue;
250 bool MediaStreamCaptureIndicator::GetAcceleratorForCommandId(
251 int command_id, ui::Accelerator* accelerator) {
252 // No accelerators for status icon context menu.
253 return false;
256 void MediaStreamCaptureIndicator::ExecuteCommand(int command_id,
257 int event_flags) {
258 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
260 const int index =
261 command_id - IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
262 DCHECK_LE(0, index);
263 DCHECK_GT(static_cast<int>(command_targets_.size()), index);
264 WebContents* const web_contents = command_targets_[index];
265 UsageMap::const_iterator it = usage_map_.find(web_contents);
266 if (it == usage_map_.end() || it->second->IsWebContentsDestroyed())
267 return;
268 web_contents->GetDelegate()->ActivateContents(web_contents);
271 void MediaStreamCaptureIndicator::CaptureDevicesOpened(
272 int render_process_id,
273 int render_view_id,
274 const content::MediaStreamDevices& devices,
275 const base::Closure& close_callback) {
276 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
277 DCHECK(!devices.empty());
279 BrowserThread::PostTask(
280 BrowserThread::UI, FROM_HERE,
281 base::Bind(&MediaStreamCaptureIndicator::AddCaptureDevices,
282 this, render_process_id, render_view_id, devices,
283 close_callback));
286 void MediaStreamCaptureIndicator::CaptureDevicesClosed(
287 int render_process_id,
288 int render_view_id,
289 const content::MediaStreamDevices& devices) {
290 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
291 DCHECK(!devices.empty());
293 BrowserThread::PostTask(
294 BrowserThread::UI, FROM_HERE,
295 base::Bind(&MediaStreamCaptureIndicator::RemoveCaptureDevices,
296 this, render_process_id, render_view_id, devices));
300 bool MediaStreamCaptureIndicator::IsCapturingUserMedia(
301 content::WebContents* web_contents) const {
302 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
304 UsageMap::const_iterator it = usage_map_.find(web_contents);
305 return (it != usage_map_.end() &&
306 (it->second->IsCapturingAudio() || it->second->IsCapturingVideo()));
309 bool MediaStreamCaptureIndicator::IsBeingMirrored(
310 content::WebContents* web_contents) const {
311 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
313 UsageMap::const_iterator it = usage_map_.find(web_contents);
314 return it != usage_map_.end() && it->second->IsMirroring();
317 void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon() {
318 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
319 if (status_icon_)
320 return;
322 // If there is no browser process, we should not create the status tray.
323 if (!g_browser_process)
324 return;
326 StatusTray* status_tray = g_browser_process->status_tray();
327 if (!status_tray)
328 return;
330 status_icon_ = status_tray->CreateStatusIcon();
332 EnsureStatusTrayIconResources();
335 void MediaStreamCaptureIndicator::EnsureStatusTrayIconResources() {
336 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
337 if (!mic_image_) {
338 mic_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
339 IDR_INFOBAR_MEDIA_STREAM_MIC);
341 if (!camera_image_) {
342 camera_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
343 IDR_INFOBAR_MEDIA_STREAM_CAMERA);
345 if (!balloon_image_) {
346 balloon_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
347 IDR_PRODUCT_LOGO_32);
349 DCHECK(mic_image_);
350 DCHECK(camera_image_);
351 DCHECK(balloon_image_);
354 void MediaStreamCaptureIndicator::ShowBalloon(
355 WebContents* web_contents, int balloon_body_message_id) {
356 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
357 DCHECK_NE(0, balloon_body_message_id);
359 // Only show the balloon for extensions.
360 const extensions::Extension* const extension = GetExtension(web_contents);
361 if (!extension) {
362 DVLOG(1) << "Balloon is shown only for extensions";
363 return;
366 string16 message =
367 l10n_util::GetStringFUTF16(balloon_body_message_id,
368 UTF8ToUTF16(extension->name()));
369 Profile* profile =
370 Profile::FromBrowserContext(web_contents->GetBrowserContext());
372 should_show_balloon_ = true;
373 extensions::ImageLoader::Get(profile)->LoadImageAsync(
374 extension,
375 extensions::IconsInfo::GetIconResource(
376 extension, 32, ExtensionIconSet::MATCH_BIGGER),
377 gfx::Size(32, 32),
378 base::Bind(&MediaStreamCaptureIndicator::OnImageLoaded,
379 this, message));
382 void MediaStreamCaptureIndicator::OnImageLoaded(
383 const string16& message,
384 const gfx::Image& image) {
385 if (!should_show_balloon_ || !status_icon_)
386 return;
388 const gfx::ImageSkia* image_skia = !image.IsEmpty() ? image.ToImageSkia() :
389 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
390 IDR_APP_DEFAULT_ICON);
391 status_icon_->DisplayBalloon(*image_skia, string16(), message);
394 void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() {
395 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
397 // Make sure images that finish loading don't cause a balloon to be shown.
398 should_show_balloon_ = false;
400 if (!status_icon_)
401 return;
403 // If there is no browser process, we should not do anything.
404 if (!g_browser_process)
405 return;
407 StatusTray* status_tray = g_browser_process->status_tray();
408 if (status_tray != NULL) {
409 status_tray->RemoveStatusIcon(status_icon_);
410 status_icon_ = NULL;
414 void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() {
415 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
416 scoped_ptr<ui::SimpleMenuModel> menu(new ui::SimpleMenuModel(this));
418 bool audio = false;
419 bool video = false;
420 int command_id = IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
421 command_targets_.clear();
423 WebContents* screen_capturer = NULL;
424 base::Closure close_callback;
426 for (UsageMap::const_iterator iter = usage_map_.begin();
427 iter != usage_map_.end(); ++iter) {
428 // Check if any audio and video devices have been used.
429 const WebContentsDeviceUsage& usage = *iter->second;
430 WebContents* const web_contents = iter->first;
431 if (usage.IsWebContentsDestroyed()) {
432 // We only show the tray icon for extensions that have not been
433 // destroyed and are capturing audio or video.
434 continue;
437 if (usage.IsCapturingScreen()) {
438 DCHECK(!screen_capturer);
439 screen_capturer = web_contents;
440 close_callback = usage.StopScreenCaptureCallback();
443 // Audio/video icon is shown only for extensions. For regular tabs, we show
444 // an indicator in the tab icon.
445 if (GetExtension(web_contents) &&
446 (usage.IsCapturingAudio() || usage.IsCapturingVideo())) {
447 audio = audio || usage.IsCapturingAudio();
448 video = video || usage.IsCapturingVideo();
450 command_targets_.push_back(web_contents);
451 menu->AddItem(command_id, GetTitle(web_contents));
453 // If reaching the maximum number, no more item will be added to the menu.
454 if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST)
455 break;
456 ++command_id;
460 if (screen_capturer) {
461 if (!screen_capture_notification_) {
462 screen_capture_notification_ = ScreenCaptureNotificationUI::Create();
463 if (!screen_capture_notification_->Show(
464 base::Bind(&MediaStreamCaptureIndicator::OnStopScreenCapture,
465 this, close_callback),
466 GetTitle(screen_capturer))) {
467 OnStopScreenCapture(close_callback);
468 screen_capture_notification_.reset();
471 } else {
472 screen_capture_notification_.reset();
475 if (command_targets_.empty()) {
476 MaybeDestroyStatusTrayIcon();
477 return;
480 // The icon will take the ownership of the passed context menu.
481 MaybeCreateStatusTrayIcon();
482 if (status_icon_) {
483 status_icon_->SetContextMenu(menu.release());
484 UpdateStatusTrayIconDisplay(audio, video);
488 void MediaStreamCaptureIndicator::UpdateStatusTrayIconDisplay(
489 bool audio, bool video) {
490 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
491 DCHECK(audio || video);
492 DCHECK(status_icon_);
493 int message_id = 0;
494 if (audio && video) {
495 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO;
496 status_icon_->SetImage(*camera_image_);
497 } else if (audio && !video) {
498 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY;
499 status_icon_->SetImage(*mic_image_);
500 } else if (!audio && video) {
501 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY;
502 status_icon_->SetImage(*camera_image_);
505 status_icon_->SetToolTip(l10n_util::GetStringFUTF16(
506 message_id, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)));
509 WebContents* MediaStreamCaptureIndicator::LookUpByKnownAlias(
510 int render_process_id, int render_view_id) const {
511 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
513 WebContents* result =
514 tab_util::GetWebContentsByID(render_process_id, render_view_id);
515 if (!result) {
516 const RenderViewIDs key(render_process_id, render_view_id);
517 AliasMap::const_iterator it = aliases_.find(key);
518 if (it != aliases_.end())
519 result = it->second;
521 return result;
524 void MediaStreamCaptureIndicator::AddCaptureDevices(
525 int render_process_id,
526 int render_view_id,
527 const content::MediaStreamDevices& devices,
528 const base::Closure& close_callback) {
529 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
531 WebContents* const web_contents =
532 LookUpByKnownAlias(render_process_id, render_view_id);
533 if (!web_contents)
534 return;
536 // Increase the usage ref-counts.
537 WebContentsDeviceUsage*& usage = usage_map_[web_contents];
538 if (!usage)
539 usage = new WebContentsDeviceUsage(web_contents);
540 const int balloon_body_message_id =
541 usage->AddDevices(devices, close_callback);
543 // Keep track of the IDs as a known alias to the WebContents instance.
544 const AliasMap::iterator insert_it = aliases_.insert(
545 make_pair(RenderViewIDs(render_process_id, render_view_id),
546 web_contents)).first;
547 DCHECK_EQ(web_contents, insert_it->second)
548 << "BUG: IDs refer to two different WebContents instances.";
550 web_contents->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
552 UpdateNotificationUserInterface();
553 if (balloon_body_message_id)
554 ShowBalloon(web_contents, balloon_body_message_id);
557 void MediaStreamCaptureIndicator::RemoveCaptureDevices(
558 int render_process_id,
559 int render_view_id,
560 const content::MediaStreamDevices& devices) {
561 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
563 WebContents* const web_contents =
564 LookUpByKnownAlias(render_process_id, render_view_id);
565 if (!web_contents)
566 return;
568 // Decrease the usage ref-counts.
569 const UsageMap::iterator it = usage_map_.find(web_contents);
570 if (it == usage_map_.end()) {
571 DLOG(FATAL) << "BUG: Attempt to remove devices more than once.";
572 return;
574 WebContentsDeviceUsage* const usage = it->second;
575 usage->RemoveDevices(devices);
577 if (!usage->IsWebContentsDestroyed())
578 web_contents->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
580 // Remove the usage and alias mappings if all the devices have been closed.
581 if (!usage->IsCapturingAudio() && !usage->IsCapturingVideo() &&
582 !usage->IsMirroring() && !usage->IsCapturingScreen()) {
583 for (AliasMap::iterator alias_it = aliases_.begin();
584 alias_it != aliases_.end(); ) {
585 if (alias_it->second == web_contents)
586 aliases_.erase(alias_it++);
587 else
588 ++alias_it;
590 delete usage;
591 usage_map_.erase(it);
594 UpdateNotificationUserInterface();
597 void MediaStreamCaptureIndicator::OnStopScreenCapture(
598 const base::Closure& stop) {
599 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
600 stop.Run();