Home | History | Annotate | Download | only in media
      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   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:
    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   virtual ~UIDelegate() {
    191     if (started_ && device_usage_.get())
    192       device_usage_->RemoveDevices(devices_);
    193   }
    194 
    195  private:
    196   // content::MediaStreamUI interface.
    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>
    215 MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream(
    216     const content::MediaStreamDevices& devices) {
    217   return scoped_ptr<content::MediaStreamUI>(new UIDelegate(
    218       weak_factory_.GetWeakPtr(), devices));
    219 }
    220 
    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 
    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 
    267 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
    268     : status_icon_(NULL),
    269       mic_image_(NULL),
    270       camera_image_(NULL) {
    271 }
    272 
    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>
    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 
    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 
    314 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 
    323 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 
    331 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 
    339 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 
    347 void MediaStreamCaptureIndicator::UnregisterWebContents(
    348     WebContents* web_contents) {
    349   usage_map_.erase(web_contents);
    350   UpdateNotificationUserInterface();
    351 }
    352 
    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 
    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 
    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 
    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 
    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