Switch global error menu icon to vectorized MD asset
[chromium-blink-merge.git] / chrome / browser / media / media_stream_capture_indicator.cc
blob0c91b5bca8c84c452c346baf0524682ab8a8f8b4
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 <string>
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/profiles/profile.h"
16 #include "chrome/browser/status_icons/status_icon.h"
17 #include "chrome/browser/status_icons/status_tray.h"
18 #include "chrome/browser/tab_contents/tab_util.h"
19 #include "chrome/common/pref_names.h"
20 #include "chrome/grit/chromium_strings.h"
21 #include "components/url_formatter/url_formatter.h"
22 #include "content/public/browser/browser_thread.h"
23 #include "content/public/browser/content_browser_client.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 "grit/theme_resources.h"
28 #include "ui/base/l10n/l10n_util.h"
29 #include "ui/base/resource/resource_bundle.h"
30 #include "ui/gfx/image/image_skia.h"
32 #if defined(ENABLE_EXTENSIONS)
33 #include "chrome/common/extensions/extension_constants.h"
34 #include "extensions/browser/extension_registry.h"
35 #include "extensions/common/extension.h"
36 #endif
38 using content::BrowserThread;
39 using content::WebContents;
41 namespace {
43 #if defined(ENABLE_EXTENSIONS)
44 const extensions::Extension* GetExtension(WebContents* web_contents) {
45 DCHECK_CURRENTLY_ON(BrowserThread::UI);
47 if (!web_contents)
48 return NULL;
50 extensions::ExtensionRegistry* registry =
51 extensions::ExtensionRegistry::Get(web_contents->GetBrowserContext());
52 return registry->enabled_extensions().GetExtensionOrAppByURL(
53 web_contents->GetURL());
56 bool IsWhitelistedExtension(const extensions::Extension* extension) {
57 DCHECK_CURRENTLY_ON(BrowserThread::UI);
59 static const char* const kExtensionWhitelist[] = {
60 extension_misc::kHotwordNewExtensionId,
63 for (size_t i = 0; i < arraysize(kExtensionWhitelist); ++i) {
64 if (extension->id() == kExtensionWhitelist[i])
65 return true;
68 return false;
70 #endif // defined(ENABLE_EXTENSIONS)
72 // Gets the security originator of the tab. It returns a string with no '/'
73 // at the end to display in the UI.
74 base::string16 GetSecurityOrigin(WebContents* web_contents) {
75 DCHECK_CURRENTLY_ON(BrowserThread::UI);
77 if (!web_contents)
78 return base::string16();
80 std::string security_origin = web_contents->GetURL().GetOrigin().spec();
82 // Remove the last character if it is a '/'.
83 if (!security_origin.empty()) {
84 std::string::iterator it = security_origin.end() - 1;
85 if (*it == '/')
86 security_origin.erase(it);
89 return base::UTF8ToUTF16(security_origin);
92 base::string16 GetTitle(WebContents* web_contents) {
93 DCHECK_CURRENTLY_ON(BrowserThread::UI);
95 if (!web_contents)
96 return base::string16();
98 #if defined(ENABLE_EXTENSIONS)
99 const extensions::Extension* const extension = GetExtension(web_contents);
100 if (extension)
101 return base::UTF8ToUTF16(extension->name());
102 #endif
104 base::string16 tab_title = web_contents->GetTitle();
106 if (tab_title.empty()) {
107 // If the page's title is empty use its security originator.
108 tab_title = GetSecurityOrigin(web_contents);
109 } else {
110 // If the page's title matches its URL, use its security originator.
111 Profile* profile =
112 Profile::FromBrowserContext(web_contents->GetBrowserContext());
113 std::string languages =
114 profile->GetPrefs()->GetString(prefs::kAcceptLanguages);
115 if (tab_title ==
116 url_formatter::FormatUrl(web_contents->GetURL(), languages))
117 tab_title = GetSecurityOrigin(web_contents);
120 return tab_title;
123 } // namespace
125 // Stores usage counts for all the capture devices associated with a single
126 // WebContents instance. Instances of this class are owned by
127 // MediaStreamCaptureIndicator. They also observe for the destruction of their
128 // corresponding WebContents and trigger their own deletion from their
129 // MediaStreamCaptureIndicator.
130 class MediaStreamCaptureIndicator::WebContentsDeviceUsage
131 : public content::WebContentsObserver {
132 public:
133 WebContentsDeviceUsage(scoped_refptr<MediaStreamCaptureIndicator> indicator,
134 WebContents* web_contents)
135 : WebContentsObserver(web_contents),
136 indicator_(indicator),
137 audio_ref_count_(0),
138 video_ref_count_(0),
139 mirroring_ref_count_(0),
140 weak_factory_(this) {
143 bool IsCapturingAudio() const { return audio_ref_count_ > 0; }
144 bool IsCapturingVideo() const { return video_ref_count_ > 0; }
145 bool IsMirroring() const { return mirroring_ref_count_ > 0; }
147 scoped_ptr<content::MediaStreamUI> RegisterMediaStream(
148 const content::MediaStreamDevices& devices);
150 // Increment ref-counts up based on the type of each device provided.
151 void AddDevices(const content::MediaStreamDevices& devices);
153 // Decrement ref-counts up based on the type of each device provided.
154 void RemoveDevices(const content::MediaStreamDevices& devices);
156 private:
157 // content::WebContentsObserver overrides.
158 void WebContentsDestroyed() override {
159 indicator_->UnregisterWebContents(web_contents());
162 scoped_refptr<MediaStreamCaptureIndicator> indicator_;
163 int audio_ref_count_;
164 int video_ref_count_;
165 int mirroring_ref_count_;
167 base::WeakPtrFactory<WebContentsDeviceUsage> weak_factory_;
169 DISALLOW_COPY_AND_ASSIGN(WebContentsDeviceUsage);
172 // Implements MediaStreamUI interface. Instances of this class are created for
173 // each MediaStream and their ownership is passed to MediaStream implementation
174 // in the content layer. Each UIDelegate keeps a weak pointer to the
175 // corresponding WebContentsDeviceUsage object to deliver updates about state of
176 // the stream.
177 class MediaStreamCaptureIndicator::UIDelegate : public content::MediaStreamUI {
178 public:
179 UIDelegate(base::WeakPtr<WebContentsDeviceUsage> device_usage,
180 const content::MediaStreamDevices& devices)
181 : device_usage_(device_usage),
182 devices_(devices),
183 started_(false) {
184 DCHECK(!devices_.empty());
187 ~UIDelegate() override {
188 if (started_ && device_usage_.get())
189 device_usage_->RemoveDevices(devices_);
192 private:
193 // content::MediaStreamUI interface.
194 gfx::NativeViewId OnStarted(const base::Closure& close_callback) override {
195 DCHECK(!started_);
196 started_ = true;
197 if (device_usage_.get())
198 device_usage_->AddDevices(devices_);
199 return 0;
202 base::WeakPtr<WebContentsDeviceUsage> device_usage_;
203 content::MediaStreamDevices devices_;
204 bool started_;
206 DISALLOW_COPY_AND_ASSIGN(UIDelegate);
210 scoped_ptr<content::MediaStreamUI>
211 MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream(
212 const content::MediaStreamDevices& devices) {
213 return make_scoped_ptr(new UIDelegate( weak_factory_.GetWeakPtr(), devices));
216 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices(
217 const content::MediaStreamDevices& devices) {
218 for (content::MediaStreamDevices::const_iterator it = devices.begin();
219 it != devices.end(); ++it) {
220 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
221 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
222 ++mirroring_ref_count_;
223 } else if (content::IsAudioInputMediaType(it->type)) {
224 ++audio_ref_count_;
225 } else if (content::IsVideoMediaType(it->type)) {
226 ++video_ref_count_;
227 } else {
228 NOTIMPLEMENTED();
232 if (web_contents())
233 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
235 indicator_->UpdateNotificationUserInterface();
238 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices(
239 const content::MediaStreamDevices& devices) {
240 for (content::MediaStreamDevices::const_iterator it = devices.begin();
241 it != devices.end(); ++it) {
242 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
243 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
244 --mirroring_ref_count_;
245 } else if (content::IsAudioInputMediaType(it->type)) {
246 --audio_ref_count_;
247 } else if (content::IsVideoMediaType(it->type)) {
248 --video_ref_count_;
249 } else {
250 NOTIMPLEMENTED();
254 DCHECK_GE(audio_ref_count_, 0);
255 DCHECK_GE(video_ref_count_, 0);
256 DCHECK_GE(mirroring_ref_count_, 0);
258 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
259 indicator_->UpdateNotificationUserInterface();
262 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
263 : status_icon_(NULL),
264 mic_image_(NULL),
265 camera_image_(NULL) {
268 MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() {
269 // The user is responsible for cleaning up by reporting the closure of any
270 // opened devices. However, there exists a race condition at shutdown: The UI
271 // thread may be stopped before CaptureDevicesClosed() posts the task to
272 // invoke DoDevicesClosedOnUIThread(). In this case, usage_map_ won't be
273 // empty like it should.
274 DCHECK(usage_map_.empty() ||
275 !BrowserThread::IsMessageLoopValid(BrowserThread::UI));
278 scoped_ptr<content::MediaStreamUI>
279 MediaStreamCaptureIndicator::RegisterMediaStream(
280 content::WebContents* web_contents,
281 const content::MediaStreamDevices& devices) {
282 WebContentsDeviceUsage* usage = usage_map_.get(web_contents);
283 if (!usage) {
284 usage = new WebContentsDeviceUsage(this, web_contents);
285 usage_map_.add(web_contents, make_scoped_ptr(usage));
287 return usage->RegisterMediaStream(devices);
290 void MediaStreamCaptureIndicator::ExecuteCommand(int command_id,
291 int event_flags) {
292 DCHECK_CURRENTLY_ON(BrowserThread::UI);
294 const int index =
295 command_id - IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
296 DCHECK_LE(0, index);
297 DCHECK_GT(static_cast<int>(command_targets_.size()), index);
298 WebContents* web_contents = command_targets_[index];
299 if (ContainsKey(usage_map_, web_contents))
300 web_contents->GetDelegate()->ActivateContents(web_contents);
303 bool MediaStreamCaptureIndicator::IsCapturingUserMedia(
304 content::WebContents* web_contents) const {
305 DCHECK_CURRENTLY_ON(BrowserThread::UI);
307 WebContentsDeviceUsage* usage = usage_map_.get(web_contents);
308 return usage && (usage->IsCapturingAudio() || usage->IsCapturingVideo());
311 bool MediaStreamCaptureIndicator::IsCapturingVideo(
312 content::WebContents* web_contents) const {
313 DCHECK_CURRENTLY_ON(BrowserThread::UI);
315 WebContentsDeviceUsage* usage = usage_map_.get(web_contents);
316 return usage && usage->IsCapturingVideo();
319 bool MediaStreamCaptureIndicator::IsCapturingAudio(
320 content::WebContents* web_contents) const {
321 DCHECK_CURRENTLY_ON(BrowserThread::UI);
323 WebContentsDeviceUsage* usage = usage_map_.get(web_contents);
324 return usage && usage->IsCapturingAudio();
327 bool MediaStreamCaptureIndicator::IsBeingMirrored(
328 content::WebContents* web_contents) const {
329 DCHECK_CURRENTLY_ON(BrowserThread::UI);
331 WebContentsDeviceUsage* usage = usage_map_.get(web_contents);
332 return usage && usage->IsMirroring();
335 void MediaStreamCaptureIndicator::UnregisterWebContents(
336 WebContents* web_contents) {
337 usage_map_.erase(web_contents);
338 UpdateNotificationUserInterface();
341 void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon(bool audio,
342 bool video) {
343 DCHECK_CURRENTLY_ON(BrowserThread::UI);
345 if (status_icon_)
346 return;
348 // If there is no browser process, we should not create the status tray.
349 if (!g_browser_process)
350 return;
352 StatusTray* status_tray = g_browser_process->status_tray();
353 if (!status_tray)
354 return;
356 EnsureStatusTrayIconResources();
358 gfx::ImageSkia image;
359 base::string16 tool_tip;
360 GetStatusTrayIconInfo(audio, video, &image, &tool_tip);
361 DCHECK(!image.isNull());
362 DCHECK(!tool_tip.empty());
364 status_icon_ = status_tray->CreateStatusIcon(
365 StatusTray::MEDIA_STREAM_CAPTURE_ICON, image, tool_tip);
368 void MediaStreamCaptureIndicator::EnsureStatusTrayIconResources() {
369 DCHECK_CURRENTLY_ON(BrowserThread::UI);
371 if (!mic_image_) {
372 mic_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
373 IDR_INFOBAR_MEDIA_STREAM_MIC);
375 if (!camera_image_) {
376 camera_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
377 IDR_INFOBAR_MEDIA_STREAM_CAMERA);
379 DCHECK(mic_image_);
380 DCHECK(camera_image_);
383 void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() {
384 DCHECK_CURRENTLY_ON(BrowserThread::UI);
386 if (!status_icon_)
387 return;
389 // If there is no browser process, we should not do anything.
390 if (!g_browser_process)
391 return;
393 StatusTray* status_tray = g_browser_process->status_tray();
394 if (status_tray != NULL) {
395 status_tray->RemoveStatusIcon(status_icon_);
396 status_icon_ = NULL;
400 void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() {
401 DCHECK_CURRENTLY_ON(BrowserThread::UI);
403 scoped_ptr<StatusIconMenuModel> menu(new StatusIconMenuModel(this));
404 bool audio = false;
405 bool video = false;
406 int command_id = IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
407 command_targets_.clear();
409 for (const auto& it : usage_map_) {
410 // Check if any audio and video devices have been used.
411 const WebContentsDeviceUsage& usage = *it.second;
412 if (!usage.IsCapturingAudio() && !usage.IsCapturingVideo())
413 continue;
415 WebContents* const web_contents = it.first;
417 // The audio/video icon is shown only for non-whitelisted extensions or on
418 // Android. For regular tabs on desktop, we show an indicator in the tab
419 // icon.
420 #if defined(ENABLE_EXTENSIONS)
421 const extensions::Extension* extension = GetExtension(web_contents);
422 if (!extension || IsWhitelistedExtension(extension))
423 continue;
424 #endif
426 audio = audio || usage.IsCapturingAudio();
427 video = video || usage.IsCapturingVideo();
429 command_targets_.push_back(web_contents);
430 menu->AddItem(command_id, GetTitle(web_contents));
432 // If the menu item is not a label, enable it.
433 menu->SetCommandIdEnabled(command_id, command_id != IDC_MinimumLabelValue);
435 // If reaching the maximum number, no more item will be added to the menu.
436 if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST)
437 break;
438 ++command_id;
441 if (command_targets_.empty()) {
442 MaybeDestroyStatusTrayIcon();
443 return;
446 // The icon will take the ownership of the passed context menu.
447 MaybeCreateStatusTrayIcon(audio, video);
448 if (status_icon_) {
449 status_icon_->SetContextMenu(menu.Pass());
453 void MediaStreamCaptureIndicator::GetStatusTrayIconInfo(
454 bool audio,
455 bool video,
456 gfx::ImageSkia* image,
457 base::string16* tool_tip) {
458 DCHECK_CURRENTLY_ON(BrowserThread::UI);
459 DCHECK(audio || video);
460 DCHECK(image);
461 DCHECK(tool_tip);
463 int message_id = 0;
464 if (audio && video) {
465 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO;
466 *image = *camera_image_;
467 } else if (audio && !video) {
468 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY;
469 *image = *mic_image_;
470 } else if (!audio && video) {
471 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY;
472 *image = *camera_image_;
475 *tool_tip = l10n_util::GetStringUTF16(message_id);