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