Update To 11.40.268.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/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)
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
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.
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
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:
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
144   bool IsCapturingAudio() const { return audio_ref_count_ > 0; }
145   bool IsCapturingVideo() const { return video_ref_count_ > 0; }
146   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.
159   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:
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
190   ~UIDelegate() override {
191     if (started_ && device_usage_.get())
192       device_usage_->RemoveDevices(devices_);
193   }
194
195  private:
196   // content::MediaStreamUI interface.
197   gfx::NativeViewId OnStarted(const base::Closure& close_callback) override {
198     DCHECK(!started_);
199     started_ = true;
200     if (device_usage_.get())
201       device_usage_->AddDevices(devices_);
202     return 0;
203   }
204
205   base::WeakPtr<WebContentsDeviceUsage> device_usage_;
206   content::MediaStreamDevices devices_;
207   bool started_;
208
209   DISALLOW_COPY_AND_ASSIGN(UIDelegate);
210 };
211
212
213 scoped_ptr<content::MediaStreamUI>
214 MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream(
215     const content::MediaStreamDevices& devices) {
216   return scoped_ptr<content::MediaStreamUI>(new UIDelegate(
217       weak_factory_.GetWeakPtr(), devices));
218 }
219
220 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices(
221     const content::MediaStreamDevices& devices) {
222   for (content::MediaStreamDevices::const_iterator it = devices.begin();
223        it != devices.end(); ++it) {
224     if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
225         it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
226       ++mirroring_ref_count_;
227     } else if (content::IsAudioInputMediaType(it->type)) {
228       ++audio_ref_count_;
229     } else if (content::IsVideoMediaType(it->type)) {
230       ++video_ref_count_;
231     } else {
232       NOTIMPLEMENTED();
233     }
234   }
235
236   if (web_contents())
237     web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
238
239   indicator_->UpdateNotificationUserInterface();
240 }
241
242 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices(
243     const content::MediaStreamDevices& devices) {
244   for (content::MediaStreamDevices::const_iterator it = devices.begin();
245        it != devices.end(); ++it) {
246     if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE ||
247         it->type == content::MEDIA_TAB_VIDEO_CAPTURE) {
248       --mirroring_ref_count_;
249     } else if (content::IsAudioInputMediaType(it->type)) {
250       --audio_ref_count_;
251     } else if (content::IsVideoMediaType(it->type)) {
252       --video_ref_count_;
253     } else {
254       NOTIMPLEMENTED();
255     }
256   }
257
258   DCHECK_GE(audio_ref_count_, 0);
259   DCHECK_GE(video_ref_count_, 0);
260   DCHECK_GE(mirroring_ref_count_, 0);
261
262   web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
263   indicator_->UpdateNotificationUserInterface();
264 }
265
266 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
267     : status_icon_(NULL),
268       mic_image_(NULL),
269       camera_image_(NULL) {
270 }
271
272 MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() {
273   // The user is responsible for cleaning up by reporting the closure of any
274   // opened devices.  However, there exists a race condition at shutdown: The UI
275   // thread may be stopped before CaptureDevicesClosed() posts the task to
276   // invoke DoDevicesClosedOnUIThread().  In this case, usage_map_ won't be
277   // empty like it should.
278   DCHECK(usage_map_.empty() ||
279          !BrowserThread::IsMessageLoopValid(BrowserThread::UI));
280
281   // Free any WebContentsDeviceUsage objects left over.
282   for (UsageMap::const_iterator it = usage_map_.begin(); it != usage_map_.end();
283        ++it) {
284     delete it->second;
285   }
286 }
287
288 scoped_ptr<content::MediaStreamUI>
289 MediaStreamCaptureIndicator::RegisterMediaStream(
290     content::WebContents* web_contents,
291     const content::MediaStreamDevices& devices) {
292   WebContentsDeviceUsage*& usage = usage_map_[web_contents];
293   if (!usage)
294     usage = new WebContentsDeviceUsage(this, web_contents);
295   return usage->RegisterMediaStream(devices);
296 }
297
298 void MediaStreamCaptureIndicator::ExecuteCommand(int command_id,
299                                                  int event_flags) {
300   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
301
302   const int index =
303       command_id - IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
304   DCHECK_LE(0, index);
305   DCHECK_GT(static_cast<int>(command_targets_.size()), index);
306   WebContents* const web_contents = command_targets_[index];
307   UsageMap::const_iterator it = usage_map_.find(web_contents);
308   if (it == usage_map_.end())
309     return;
310   web_contents->GetDelegate()->ActivateContents(web_contents);
311 }
312
313 bool MediaStreamCaptureIndicator::IsCapturingUserMedia(
314     content::WebContents* web_contents) const {
315   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
316
317   UsageMap::const_iterator it = usage_map_.find(web_contents);
318   return (it != usage_map_.end() &&
319           (it->second->IsCapturingAudio() || it->second->IsCapturingVideo()));
320 }
321
322 bool MediaStreamCaptureIndicator::IsCapturingVideo(
323     content::WebContents* web_contents) const {
324   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
325
326   UsageMap::const_iterator it = usage_map_.find(web_contents);
327   return (it != usage_map_.end() && it->second->IsCapturingVideo());
328 }
329
330 bool MediaStreamCaptureIndicator::IsCapturingAudio(
331     content::WebContents* web_contents) const {
332   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
333
334   UsageMap::const_iterator it = usage_map_.find(web_contents);
335   return (it != usage_map_.end() && it->second->IsCapturingAudio());
336 }
337
338 bool MediaStreamCaptureIndicator::IsBeingMirrored(
339     content::WebContents* web_contents) const {
340   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
341
342   UsageMap::const_iterator it = usage_map_.find(web_contents);
343   return it != usage_map_.end() && it->second->IsMirroring();
344 }
345
346 void MediaStreamCaptureIndicator::UnregisterWebContents(
347     WebContents* web_contents) {
348   usage_map_.erase(web_contents);
349   UpdateNotificationUserInterface();
350 }
351
352 void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon(bool audio,
353                                                             bool video) {
354   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
355   if (status_icon_)
356     return;
357
358   // If there is no browser process, we should not create the status tray.
359   if (!g_browser_process)
360     return;
361
362   StatusTray* status_tray = g_browser_process->status_tray();
363   if (!status_tray)
364     return;
365
366   EnsureStatusTrayIconResources();
367
368   gfx::ImageSkia image;
369   base::string16 tool_tip;
370   GetStatusTrayIconInfo(audio, video, &image, &tool_tip);
371   DCHECK(!image.isNull());
372   DCHECK(!tool_tip.empty());
373
374   status_icon_ = status_tray->CreateStatusIcon(
375       StatusTray::MEDIA_STREAM_CAPTURE_ICON, image, tool_tip);
376 }
377
378 void MediaStreamCaptureIndicator::EnsureStatusTrayIconResources() {
379   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
380   if (!mic_image_) {
381     mic_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
382         IDR_INFOBAR_MEDIA_STREAM_MIC);
383   }
384   if (!camera_image_) {
385     camera_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
386         IDR_INFOBAR_MEDIA_STREAM_CAMERA);
387   }
388   DCHECK(mic_image_);
389   DCHECK(camera_image_);
390 }
391
392 void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() {
393   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
394
395   if (!status_icon_)
396     return;
397
398   // If there is no browser process, we should not do anything.
399   if (!g_browser_process)
400     return;
401
402   StatusTray* status_tray = g_browser_process->status_tray();
403   if (status_tray != NULL) {
404     status_tray->RemoveStatusIcon(status_icon_);
405     status_icon_ = NULL;
406   }
407 }
408
409 void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() {
410   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
411   scoped_ptr<StatusIconMenuModel> menu(new StatusIconMenuModel(this));
412
413   bool audio = false;
414   bool video = false;
415   int command_id = IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
416   command_targets_.clear();
417
418   for (UsageMap::const_iterator iter = usage_map_.begin();
419        iter != usage_map_.end(); ++iter) {
420     // Check if any audio and video devices have been used.
421     const WebContentsDeviceUsage& usage = *iter->second;
422     if (!usage.IsCapturingAudio() && !usage.IsCapturingVideo())
423       continue;
424
425     WebContents* const web_contents = iter->first;
426
427     // The audio/video icon is shown only for non-whitelisted extensions or on
428     // Android. For regular tabs on desktop, we show an indicator in the tab
429     // icon.
430 #if defined(ENABLE_EXTENSIONS)
431     const extensions::Extension* extension = GetExtension(web_contents);
432     if (!extension || IsWhitelistedExtension(extension))
433       continue;
434 #endif
435
436     audio = audio || usage.IsCapturingAudio();
437     video = video || usage.IsCapturingVideo();
438
439     command_targets_.push_back(web_contents);
440     menu->AddItem(command_id, GetTitle(web_contents));
441
442     // If the menu item is not a label, enable it.
443     menu->SetCommandIdEnabled(command_id,
444                               command_id != IDC_MinimumLabelValue);
445
446     // If reaching the maximum number, no more item will be added to the menu.
447     if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST)
448       break;
449     ++command_id;
450   }
451
452   if (command_targets_.empty()) {
453     MaybeDestroyStatusTrayIcon();
454     return;
455   }
456
457   // The icon will take the ownership of the passed context menu.
458   MaybeCreateStatusTrayIcon(audio, video);
459   if (status_icon_) {
460     status_icon_->SetContextMenu(menu.Pass());
461   }
462 }
463
464 void MediaStreamCaptureIndicator::GetStatusTrayIconInfo(
465     bool audio,
466     bool video,
467     gfx::ImageSkia* image,
468     base::string16* tool_tip) {
469   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
470   DCHECK(audio || video);
471
472   int message_id = 0;
473   if (audio && video) {
474     message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO;
475     *image = *camera_image_;
476   } else if (audio && !video) {
477     message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY;
478     *image = *mic_image_;
479   } else if (!audio && video) {
480     message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY;
481     *image = *camera_image_;
482   }
483
484   *tool_tip = l10n_util::GetStringUTF16(message_id);
485 }