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/pref_names.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 "extensions/common/extension.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
36using content::BrowserThread;
37using content::WebContents;
38
39namespace {
40
41const 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.
62base::string16 GetSecurityOrigin(WebContents* web_contents) {
63  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
64
65  if (!web_contents)
66    return base::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
80base::string16 GetTitle(WebContents* web_contents) {
81  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
82
83  if (!web_contents)
84    return base::string16();
85
86  const extensions::Extension* const extension = GetExtension(web_contents);
87  if (extension)
88    return UTF8ToUTF16(extension->name());
89
90  base::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.
115class 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.
164class 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
197scoped_ptr<content::MediaStreamUI>
198MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream(
199    const content::MediaStreamDevices& devices) {
200  return scoped_ptr<content::MediaStreamUI>(new UIDelegate(
201      weak_factory_.GetWeakPtr(), devices));
202}
203
204void 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
226void 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
250MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
251    : status_icon_(NULL),
252      mic_image_(NULL),
253      camera_image_(NULL) {
254}
255
256MediaStreamCaptureIndicator::~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
272scoped_ptr<content::MediaStreamUI>
273MediaStreamCaptureIndicator::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
282void 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
297bool 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
306bool 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
314bool 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
322bool 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
330void MediaStreamCaptureIndicator::UnregisterWebContents(
331    WebContents* web_contents) {
332  usage_map_.erase(web_contents);
333  UpdateNotificationUserInterface();
334}
335
336void 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  base::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
362void 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
376void 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
393void 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
444void MediaStreamCaptureIndicator::GetStatusTrayIconInfo(
445    bool audio,
446    bool video,
447    gfx::ImageSkia* image,
448    base::string16* tool_tip) {
449  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
450  DCHECK(audio || video);
451
452  int message_id = 0;
453  if (audio && video) {
454    message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO;
455    *image = *camera_image_;
456  } else if (audio && !video) {
457    message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY;
458    *image = *mic_image_;
459  } else if (!audio && video) {
460    message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY;
461    *image = *camera_image_;
462  }
463
464  *tool_tip = l10n_util::GetStringUTF16(message_id);
465}
466