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