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.
4
5 #include "chrome/browser/media/media_stream_capture_indicator.h"
6
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/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 "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 "grit/theme_resources.h"
28 #include "net/base/net_util.h"
29 #include "ui/base/l10n/l10n_util.h"
30 #include "ui/base/resource/resource_bundle.h"
31 #include "ui/gfx/image/image_skia.h"
32
33 #if defined(ENABLE_EXTENSIONS)
34 #include "chrome/common/extensions/extension_constants.h"
35 #include "extensions/browser/extension_registry.h"
36 #include "extensions/common/extension.h"
37 #endif
38
39 using content::BrowserThread;
40 using content::WebContents;
41
42 namespace {
43
44 #if defined(ENABLE_EXTENSIONS)
GetExtension(WebContents * web_contents)45 const extensions::Extension* GetExtension(WebContents* web_contents) {
46 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
47
48 if (!web_contents)
49 return NULL;
50
51 extensions::ExtensionRegistry* registry =
52 extensions::ExtensionRegistry::Get(web_contents->GetBrowserContext());
53 return registry->enabled_extensions().GetExtensionOrAppByURL(
54 web_contents->GetURL());
55 }
56
IsWhitelistedExtension(const extensions::Extension * extension)57 bool IsWhitelistedExtension(const extensions::Extension* extension) {
58 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
59
60 static const char* const kExtensionWhitelist[] = {
61 extension_misc::kHotwordExtensionId,
62 };
63
64 for (size_t i = 0; i < arraysize(kExtensionWhitelist); ++i) {
65 if (extension->id() == kExtensionWhitelist[i])
66 return true;
67 }
68
69 return false;
70 }
71 #endif // defined(ENABLE_EXTENSIONS)
72
73 // Gets the security originator of the tab. It returns a string with no '/'
74 // at the end to display in the UI.
GetSecurityOrigin(WebContents * web_contents)75 base::string16 GetSecurityOrigin(WebContents* web_contents) {
76 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
77
78 if (!web_contents)
79 return base::string16();
80
81 std::string security_origin = web_contents->GetURL().GetOrigin().spec();
82
83 // Remove the last character if it is a '/'.
84 if (!security_origin.empty()) {
85 std::string::iterator it = security_origin.end() - 1;
86 if (*it == '/')
87 security_origin.erase(it);
88 }
89
90 return base::UTF8ToUTF16(security_origin);
91 }
92
GetTitle(WebContents * web_contents)93 base::string16 GetTitle(WebContents* web_contents) {
94 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
95
96 if (!web_contents)
97 return base::string16();
98
99 #if defined(ENABLE_EXTENSIONS)
100 const extensions::Extension* const extension = GetExtension(web_contents);
101 if (extension)
102 return base::UTF8ToUTF16(extension->name());
103 #endif
104
105 base::string16 tab_title = web_contents->GetTitle();
106
107 if (tab_title.empty()) {
108 // If the page's title is empty use its security originator.
109 tab_title = GetSecurityOrigin(web_contents);
110 } else {
111 // If the page's title matches its URL, use its security originator.
112 Profile* profile =
113 Profile::FromBrowserContext(web_contents->GetBrowserContext());
114 std::string languages =
115 profile->GetPrefs()->GetString(prefs::kAcceptLanguages);
116 if (tab_title == net::FormatUrl(web_contents->GetURL(), languages))
117 tab_title = GetSecurityOrigin(web_contents);
118 }
119
120 return tab_title;
121 }
122
123 } // namespace
124
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 the
128 // WebContents instances and delete themselves when corresponding WebContents is
129 // deleted.
130 class MediaStreamCaptureIndicator::WebContentsDeviceUsage
131 : public content::WebContentsObserver {
132 public:
WebContentsDeviceUsage(scoped_refptr<MediaStreamCaptureIndicator> indicator,WebContents * web_contents)133 explicit WebContentsDeviceUsage(
134 scoped_refptr<MediaStreamCaptureIndicator> indicator,
135 WebContents* web_contents)
136 : WebContentsObserver(web_contents),
137 indicator_(indicator),
138 audio_ref_count_(0),
139 video_ref_count_(0),
140 mirroring_ref_count_(0),
141 weak_factory_(this) {
142 }
143
IsCapturingAudio() const144 bool IsCapturingAudio() const { return audio_ref_count_ > 0; }
IsCapturingVideo() const145 bool IsCapturingVideo() const { return video_ref_count_ > 0; }
IsMirroring() const146 bool IsMirroring() const { return mirroring_ref_count_ > 0; }
147
148 scoped_ptr<content::MediaStreamUI> RegisterMediaStream(
149 const content::MediaStreamDevices& devices);
150
151 // Increment ref-counts up based on the type of each device provided.
152 void AddDevices(const content::MediaStreamDevices& devices);
153
154 // Decrement ref-counts up based on the type of each device provided.
155 void RemoveDevices(const content::MediaStreamDevices& devices);
156
157 private:
158 // content::WebContentsObserver overrides.
WebContentsDestroyed()159 virtual void WebContentsDestroyed() OVERRIDE {
160 indicator_->UnregisterWebContents(web_contents());
161 delete this;
162 }
163
164 scoped_refptr<MediaStreamCaptureIndicator> indicator_;
165 int audio_ref_count_;
166 int video_ref_count_;
167 int mirroring_ref_count_;
168
169 base::WeakPtrFactory<WebContentsDeviceUsage> weak_factory_;
170
171 DISALLOW_COPY_AND_ASSIGN(WebContentsDeviceUsage);
172 };
173
174 // Implements MediaStreamUI interface. Instances of this class are created for
175 // each MediaStream and their ownership is passed to MediaStream implementation
176 // in the content layer. Each UIDelegate keeps a weak pointer to the
177 // corresponding WebContentsDeviceUsage object to deliver updates about state of
178 // the stream.
179 class MediaStreamCaptureIndicator::UIDelegate
180 : public content::MediaStreamUI {
181 public:
UIDelegate(base::WeakPtr<WebContentsDeviceUsage> device_usage,const content::MediaStreamDevices & devices)182 UIDelegate(base::WeakPtr<WebContentsDeviceUsage> device_usage,
183 const content::MediaStreamDevices& devices)
184 : device_usage_(device_usage),
185 devices_(devices),
186 started_(false) {
187 DCHECK(!devices_.empty());
188 }
189
~UIDelegate()190 virtual ~UIDelegate() {
191 if (started_ && device_usage_.get())
192 device_usage_->RemoveDevices(devices_);
193 }
194
195 private:
196 // content::MediaStreamUI interface.
OnStarted(const base::Closure & close_callback)197 virtual gfx::NativeViewId OnStarted(const base::Closure& close_callback)
198 OVERRIDE {
199 DCHECK(!started_);
200 started_ = true;
201 if (device_usage_.get())
202 device_usage_->AddDevices(devices_);
203 return 0;
204 }
205
206 base::WeakPtr<WebContentsDeviceUsage> device_usage_;
207 content::MediaStreamDevices devices_;
208 bool started_;
209
210 DISALLOW_COPY_AND_ASSIGN(UIDelegate);
211 };
212
213
214 scoped_ptr<content::MediaStreamUI>
RegisterMediaStream(const content::MediaStreamDevices & devices)215 MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream(
216 const content::MediaStreamDevices& devices) {
217 return scoped_ptr<content::MediaStreamUI>(new UIDelegate(
218 weak_factory_.GetWeakPtr(), devices));
219 }
220
AddDevices(const content::MediaStreamDevices & devices)221 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices(
222 const content::MediaStreamDevices& devices) {
223 for (content::MediaStreamDevices::const_iterator it = devices.begin();
224 it != devices.end(); ++it) {
225 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
226 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
227 ++mirroring_ref_count_;
228 } else if (content::IsAudioInputMediaType(it->type)) {
229 ++audio_ref_count_;
230 } else if (content::IsVideoMediaType(it->type)) {
231 ++video_ref_count_;
232 } else {
233 NOTIMPLEMENTED();
234 }
235 }
236
237 if (web_contents())
238 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
239
240 indicator_->UpdateNotificationUserInterface();
241 }
242
RemoveDevices(const content::MediaStreamDevices & devices)243 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices(
244 const content::MediaStreamDevices& devices) {
245 for (content::MediaStreamDevices::const_iterator it = devices.begin();
246 it != devices.end(); ++it) {
247 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
248 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
249 --mirroring_ref_count_;
250 } else if (content::IsAudioInputMediaType(it->type)) {
251 --audio_ref_count_;
252 } else if (content::IsVideoMediaType(it->type)) {
253 --video_ref_count_;
254 } else {
255 NOTIMPLEMENTED();
256 }
257 }
258
259 DCHECK_GE(audio_ref_count_, 0);
260 DCHECK_GE(video_ref_count_, 0);
261 DCHECK_GE(mirroring_ref_count_, 0);
262
263 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
264 indicator_->UpdateNotificationUserInterface();
265 }
266
MediaStreamCaptureIndicator()267 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
268 : status_icon_(NULL),
269 mic_image_(NULL),
270 camera_image_(NULL) {
271 }
272
~MediaStreamCaptureIndicator()273 MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() {
274 // The user is responsible for cleaning up by reporting the closure of any
275 // opened devices. However, there exists a race condition at shutdown: The UI
276 // thread may be stopped before CaptureDevicesClosed() posts the task to
277 // invoke DoDevicesClosedOnUIThread(). In this case, usage_map_ won't be
278 // empty like it should.
279 DCHECK(usage_map_.empty() ||
280 !BrowserThread::IsMessageLoopValid(BrowserThread::UI));
281
282 // Free any WebContentsDeviceUsage objects left over.
283 for (UsageMap::const_iterator it = usage_map_.begin(); it != usage_map_.end();
284 ++it) {
285 delete it->second;
286 }
287 }
288
289 scoped_ptr<content::MediaStreamUI>
RegisterMediaStream(content::WebContents * web_contents,const content::MediaStreamDevices & devices)290 MediaStreamCaptureIndicator::RegisterMediaStream(
291 content::WebContents* web_contents,
292 const content::MediaStreamDevices& devices) {
293 WebContentsDeviceUsage*& usage = usage_map_[web_contents];
294 if (!usage)
295 usage = new WebContentsDeviceUsage(this, web_contents);
296 return usage->RegisterMediaStream(devices);
297 }
298
ExecuteCommand(int command_id,int event_flags)299 void MediaStreamCaptureIndicator::ExecuteCommand(int command_id,
300 int event_flags) {
301 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
302
303 const int index =
304 command_id - IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
305 DCHECK_LE(0, index);
306 DCHECK_GT(static_cast<int>(command_targets_.size()), index);
307 WebContents* const web_contents = command_targets_[index];
308 UsageMap::const_iterator it = usage_map_.find(web_contents);
309 if (it == usage_map_.end())
310 return;
311 web_contents->GetDelegate()->ActivateContents(web_contents);
312 }
313
IsCapturingUserMedia(content::WebContents * web_contents) const314 bool MediaStreamCaptureIndicator::IsCapturingUserMedia(
315 content::WebContents* web_contents) const {
316 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
317
318 UsageMap::const_iterator it = usage_map_.find(web_contents);
319 return (it != usage_map_.end() &&
320 (it->second->IsCapturingAudio() || it->second->IsCapturingVideo()));
321 }
322
IsCapturingVideo(content::WebContents * web_contents) const323 bool MediaStreamCaptureIndicator::IsCapturingVideo(
324 content::WebContents* web_contents) const {
325 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
326
327 UsageMap::const_iterator it = usage_map_.find(web_contents);
328 return (it != usage_map_.end() && it->second->IsCapturingVideo());
329 }
330
IsCapturingAudio(content::WebContents * web_contents) const331 bool MediaStreamCaptureIndicator::IsCapturingAudio(
332 content::WebContents* web_contents) const {
333 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
334
335 UsageMap::const_iterator it = usage_map_.find(web_contents);
336 return (it != usage_map_.end() && it->second->IsCapturingAudio());
337 }
338
IsBeingMirrored(content::WebContents * web_contents) const339 bool MediaStreamCaptureIndicator::IsBeingMirrored(
340 content::WebContents* web_contents) const {
341 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
342
343 UsageMap::const_iterator it = usage_map_.find(web_contents);
344 return it != usage_map_.end() && it->second->IsMirroring();
345 }
346
UnregisterWebContents(WebContents * web_contents)347 void MediaStreamCaptureIndicator::UnregisterWebContents(
348 WebContents* web_contents) {
349 usage_map_.erase(web_contents);
350 UpdateNotificationUserInterface();
351 }
352
MaybeCreateStatusTrayIcon(bool audio,bool video)353 void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon(bool audio,
354 bool video) {
355 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
356 if (status_icon_)
357 return;
358
359 // If there is no browser process, we should not create the status tray.
360 if (!g_browser_process)
361 return;
362
363 StatusTray* status_tray = g_browser_process->status_tray();
364 if (!status_tray)
365 return;
366
367 EnsureStatusTrayIconResources();
368
369 gfx::ImageSkia image;
370 base::string16 tool_tip;
371 GetStatusTrayIconInfo(audio, video, &image, &tool_tip);
372 DCHECK(!image.isNull());
373 DCHECK(!tool_tip.empty());
374
375 status_icon_ = status_tray->CreateStatusIcon(
376 StatusTray::MEDIA_STREAM_CAPTURE_ICON, image, tool_tip);
377 }
378
EnsureStatusTrayIconResources()379 void MediaStreamCaptureIndicator::EnsureStatusTrayIconResources() {
380 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
381 if (!mic_image_) {
382 mic_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
383 IDR_INFOBAR_MEDIA_STREAM_MIC);
384 }
385 if (!camera_image_) {
386 camera_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
387 IDR_INFOBAR_MEDIA_STREAM_CAMERA);
388 }
389 DCHECK(mic_image_);
390 DCHECK(camera_image_);
391 }
392
MaybeDestroyStatusTrayIcon()393 void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() {
394 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
395
396 if (!status_icon_)
397 return;
398
399 // If there is no browser process, we should not do anything.
400 if (!g_browser_process)
401 return;
402
403 StatusTray* status_tray = g_browser_process->status_tray();
404 if (status_tray != NULL) {
405 status_tray->RemoveStatusIcon(status_icon_);
406 status_icon_ = NULL;
407 }
408 }
409
UpdateNotificationUserInterface()410 void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() {
411 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
412 scoped_ptr<StatusIconMenuModel> menu(new StatusIconMenuModel(this));
413
414 bool audio = false;
415 bool video = false;
416 int command_id = IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
417 command_targets_.clear();
418
419 for (UsageMap::const_iterator iter = usage_map_.begin();
420 iter != usage_map_.end(); ++iter) {
421 // Check if any audio and video devices have been used.
422 const WebContentsDeviceUsage& usage = *iter->second;
423 if (!usage.IsCapturingAudio() && !usage.IsCapturingVideo())
424 continue;
425
426 WebContents* const web_contents = iter->first;
427
428 // The audio/video icon is shown only for non-whitelisted extensions or on
429 // Android. For regular tabs on desktop, we show an indicator in the tab
430 // icon.
431 #if defined(ENABLE_EXTENSIONS)
432 const extensions::Extension* extension = GetExtension(web_contents);
433 if (!extension || IsWhitelistedExtension(extension))
434 continue;
435 #endif
436
437 audio = audio || usage.IsCapturingAudio();
438 video = video || usage.IsCapturingVideo();
439
440 command_targets_.push_back(web_contents);
441 menu->AddItem(command_id, GetTitle(web_contents));
442
443 // If the menu item is not a label, enable it.
444 menu->SetCommandIdEnabled(command_id,
445 command_id != IDC_MinimumLabelValue);
446
447 // If reaching the maximum number, no more item will be added to the menu.
448 if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST)
449 break;
450 ++command_id;
451 }
452
453 if (command_targets_.empty()) {
454 MaybeDestroyStatusTrayIcon();
455 return;
456 }
457
458 // The icon will take the ownership of the passed context menu.
459 MaybeCreateStatusTrayIcon(audio, video);
460 if (status_icon_) {
461 status_icon_->SetContextMenu(menu.Pass());
462 }
463 }
464
GetStatusTrayIconInfo(bool audio,bool video,gfx::ImageSkia * image,base::string16 * tool_tip)465 void MediaStreamCaptureIndicator::GetStatusTrayIconInfo(
466 bool audio,
467 bool video,
468 gfx::ImageSkia* image,
469 base::string16* tool_tip) {
470 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
471 DCHECK(audio || video);
472
473 int message_id = 0;
474 if (audio && video) {
475 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO;
476 *image = *camera_image_;
477 } else if (audio && !video) {
478 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY;
479 *image = *mic_image_;
480 } else if (!audio && video) {
481 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY;
482 *image = *camera_image_;
483 }
484
485 *tool_tip = l10n_util::GetStringUTF16(message_id);
486 }
487