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/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::IsAudioInputMediaType(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::IsAudioInputMediaType(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 }
    492