blob: 0232806ec058c1bb3ec1834590c6ce2eb849369c [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/media/media_stream_capture_indicator.h"
#include "base/bind.h"
#include "base/i18n/rtl.h"
#include "base/logging.h"
#include "base/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/status_icons/status_icon.h"
#include "chrome/browser/status_icons/status_tray.h"
#include "chrome/browser/tab_contents/tab_util.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/common/media_stream_request.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "net/base/net_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
using content::BrowserThread;
using content::WebContents;
namespace {
const extensions::Extension* GetExtension(int render_process_id,
int render_view_id) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
WebContents* web_contents = tab_util::GetWebContentsByID(
render_process_id, render_view_id);
if (!web_contents)
return NULL;
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
if (!profile)
return NULL;
ExtensionService* extension_service = profile->GetExtensionService();
if (!extension_service)
return NULL;
return extension_service->extensions()->GetExtensionOrAppByURL(
ExtensionURLInfo(web_contents->GetURL()));
}
// Gets the security originator of the tab. It returns a string with no '/'
// at the end to display in the UI.
string16 GetSecurityOrigin(int render_process_id, int render_view_id) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
WebContents* tab_content = tab_util::GetWebContentsByID(
render_process_id, render_view_id);
if (!tab_content)
return string16();
std::string security_origin = tab_content->GetURL().GetOrigin().spec();
// Remove the last character if it is a '/'.
if (!security_origin.empty()) {
std::string::iterator it = security_origin.end() - 1;
if (*it == '/')
security_origin.erase(it);
}
return UTF8ToUTF16(security_origin);
}
string16 GetTitle(int render_process_id, int render_view_id) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
const extensions::Extension* extension =
GetExtension(render_process_id, render_view_id);
if (extension)
return UTF8ToUTF16(extension->name());
WebContents* tab_content = tab_util::GetWebContentsByID(
render_process_id, render_view_id);
if (!tab_content)
return string16();
string16 tab_title = tab_content->GetTitle();
if (tab_title.empty()) {
// If the page's title is empty use its security originator.
tab_title = GetSecurityOrigin(render_process_id, render_view_id);
} else {
// If the page's title matches its URL, use its security originator.
std::string languages =
content::GetContentClient()->browser()->GetAcceptLangs(
tab_content->GetBrowserContext());
if (tab_title == net::FormatUrl(tab_content->GetURL(), languages))
tab_title = GetSecurityOrigin(render_process_id, render_view_id);
}
return tab_title;
}
} // namespace
MediaStreamCaptureIndicator::TabEquals::TabEquals(int render_process_id,
int render_view_id)
: render_process_id_(render_process_id),
render_view_id_(render_view_id) {}
bool MediaStreamCaptureIndicator::TabEquals::operator() (
const MediaStreamCaptureIndicator::CaptureDeviceTab& tab) {
return (render_process_id_ == tab.render_process_id &&
render_view_id_ == tab.render_view_id);
}
MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
: status_icon_(NULL),
mic_image_(NULL),
camera_image_(NULL),
balloon_image_(NULL),
request_index_(0) {
}
MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() {
// The user is responsible for cleaning up by closing all the opened devices.
DCHECK(tabs_.empty());
}
bool MediaStreamCaptureIndicator::IsCommandIdChecked(
int command_id) const {
NOTIMPLEMENTED() << "There are no checked items in the MediaStream menu.";
return false;
}
bool MediaStreamCaptureIndicator::IsCommandIdEnabled(
int command_id) const {
return command_id != IDC_MinimumLabelValue;
}
bool MediaStreamCaptureIndicator::GetAcceleratorForCommandId(
int command_id, ui::Accelerator* accelerator) {
// No accelerators for status icon context menu.
return false;
}
void MediaStreamCaptureIndicator::ExecuteCommand(int command_id) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(command_id >= IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST &&
command_id <= IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST);
int index = command_id - IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
WebContents* web_content = tab_util::GetWebContentsByID(
tabs_[index].render_process_id, tabs_[index].render_view_id);
DCHECK(web_content);
if (!web_content) {
NOTREACHED();
return;
}
web_content->GetDelegate()->ActivateContents(web_content);
}
void MediaStreamCaptureIndicator::CaptureDevicesOpened(
int render_process_id,
int render_view_id,
const content::MediaStreamDevices& devices) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
DCHECK(!devices.empty());
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&MediaStreamCaptureIndicator::DoDevicesOpenedOnUIThread,
this, render_process_id, render_view_id, devices));
}
void MediaStreamCaptureIndicator::CaptureDevicesClosed(
int render_process_id,
int render_view_id,
const content::MediaStreamDevices& devices) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
DCHECK(!devices.empty());
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&MediaStreamCaptureIndicator::DoDevicesClosedOnUIThread,
this, render_process_id, render_view_id, devices));
}
void MediaStreamCaptureIndicator::DoDevicesOpenedOnUIThread(
int render_process_id,
int render_view_id,
const content::MediaStreamDevices& devices) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
CreateStatusTray();
// If we don't have a status icon or one could not be created successfully,
// then no need to continue.
if (!status_icon_)
return;
AddCaptureDeviceTab(render_process_id, render_view_id, devices);
}
void MediaStreamCaptureIndicator::DoDevicesClosedOnUIThread(
int render_process_id,
int render_view_id,
const content::MediaStreamDevices& devices) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (!status_icon_)
return;
RemoveCaptureDeviceTab(render_process_id, render_view_id, devices);
}
void MediaStreamCaptureIndicator::CreateStatusTray() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (status_icon_)
return;
// If there is no browser process, we should not create the status tray.
if (!g_browser_process)
return;
StatusTray* status_tray = g_browser_process->status_tray();
if (!status_tray)
return;
status_icon_ = status_tray->CreateStatusIcon();
EnsureStatusTrayIconResources();
EnsureImageLoadingTracker();
}
void MediaStreamCaptureIndicator::EnsureStatusTrayIconResources() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (!mic_image_) {
mic_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
IDR_INFOBAR_MEDIA_STREAM_MIC);
}
if (!camera_image_) {
camera_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
IDR_INFOBAR_MEDIA_STREAM_CAMERA);
}
if (!balloon_image_) {
balloon_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
IDR_PRODUCT_LOGO_32);
}
DCHECK(mic_image_);
DCHECK(camera_image_);
DCHECK(balloon_image_);
}
void MediaStreamCaptureIndicator::ShowBalloon(
int render_process_id,
int render_view_id,
bool audio,
bool video) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(audio || video);
int message_id = IDS_MEDIA_STREAM_STATUS_TRAY_BALLOON_BODY_AUDIO_AND_VIDEO;
if (audio && !video)
message_id = IDS_MEDIA_STREAM_STATUS_TRAY_BALLOON_BODY_AUDIO_ONLY;
else if (!audio && video)
message_id = IDS_MEDIA_STREAM_STATUS_TRAY_BALLOON_BODY_VIDEO_ONLY;
const extensions::Extension* extension =
GetExtension(render_process_id, render_view_id);
if (extension) {
pending_messages_[request_index_++] =
l10n_util::GetStringFUTF16(message_id,
UTF8ToUTF16(extension->name()));
tracker_->LoadImage(
extension,
extension->GetIconResource(32, ExtensionIconSet::MATCH_BIGGER),
gfx::Size(32, 32),
ImageLoadingTracker::CACHE);
return;
}
string16 title = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME);
string16 body = l10n_util::GetStringFUTF16(message_id,
GetSecurityOrigin(render_process_id, render_view_id));
status_icon_->DisplayBalloon(*balloon_image_->bitmap(), title, body);
}
void MediaStreamCaptureIndicator::OnImageLoaded(
const gfx::Image& image,
const std::string& extension_id,
int index) {
string16 message;
message.swap(pending_messages_[index]);
pending_messages_.erase(index);
const gfx::ImageSkia* image_skia = !image.IsEmpty() ? image.ToImageSkia() :
ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
IDR_APP_DEFAULT_ICON);
status_icon_->DisplayBalloon(*image_skia->bitmap(), string16(), message);
}
void MediaStreamCaptureIndicator::Hide() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(tabs_.empty());
// We have to destroy |tracker_| on the UI thread.
tracker_.reset();
if (!status_icon_)
return;
// If there is no browser process, we should not do anything.
if (!g_browser_process)
return;
StatusTray* status_tray = g_browser_process->status_tray();
if (status_tray != NULL) {
status_tray->RemoveStatusIcon(status_icon_);
status_icon_ = NULL;
}
}
void MediaStreamCaptureIndicator::UpdateStatusTrayIconContextMenu() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
scoped_ptr<ui::SimpleMenuModel> menu(new ui::SimpleMenuModel(this));
bool audio = false;
bool video = false;
int command_id = IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST;
for (CaptureDeviceTabs::iterator iter = tabs_.begin();
iter != tabs_.end();) {
string16 tab_title = GetTitle(iter->render_process_id,
iter->render_view_id);
if (tab_title.empty()) {
// Delete the entry since the tab has gone away.
iter = tabs_.erase(iter);
continue;
}
// Check if any audio and video devices have been used.
audio = audio || iter->audio_ref_count > 0;
video = video || iter->video_ref_count > 0;
string16 message = l10n_util::GetStringFUTF16(
IDS_MEDIA_STREAM_STATUS_TRAY_MENU_ITEM, tab_title);
menu->AddItem(command_id, message);
// If reaching the maximum number, no more item will be added to the menu.
if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST)
break;
++command_id;
++iter;
}
if (!audio && !video) {
Hide();
return;
}
// The icon will take the ownership of the passed context menu.
status_icon_->SetContextMenu(menu.release());
UpdateStatusTrayIconDisplay(audio, video);
}
void MediaStreamCaptureIndicator::UpdateStatusTrayIconDisplay(
bool audio, bool video) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(audio || video);
int message_id = 0;
if (audio && video) {
message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO;
status_icon_->SetImage(*camera_image_->bitmap());
} else if (audio && !video) {
message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY;
status_icon_->SetImage(*mic_image_->bitmap());
} else if (!audio && video) {
message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY;
status_icon_->SetImage(*camera_image_->bitmap());
}
status_icon_->SetToolTip(l10n_util::GetStringFUTF16(
message_id, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)));
}
void MediaStreamCaptureIndicator::AddCaptureDeviceTab(
int render_process_id,
int render_view_id,
const content::MediaStreamDevices& devices) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
CaptureDeviceTabs::iterator iter = std::find_if(
tabs_.begin(), tabs_.end(), TabEquals(render_process_id, render_view_id));
if (iter == tabs_.end()) {
tabs_.push_back(CaptureDeviceTab(render_process_id, render_view_id));
iter = tabs_.end() - 1;
}
bool audio = false;
bool video = false;
content::MediaStreamDevices::const_iterator dev = devices.begin();
for (; dev != devices.end(); ++dev) {
if (content::IsAudioMediaType(dev->type)) {
++iter->audio_ref_count;
audio = true;
} else if (content::IsVideoMediaType(dev->type)) {
++iter->video_ref_count;
video = true;
} else {
NOTIMPLEMENTED();
}
}
UpdateStatusTrayIconContextMenu();
ShowBalloon(render_process_id, render_view_id, audio, video);
}
void MediaStreamCaptureIndicator::RemoveCaptureDeviceTab(
int render_process_id,
int render_view_id,
const content::MediaStreamDevices& devices) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
CaptureDeviceTabs::iterator iter = std::find_if(
tabs_.begin(), tabs_.end(), TabEquals(render_process_id, render_view_id));
if (iter != tabs_.end()) {
content::MediaStreamDevices::const_iterator dev = devices.begin();
for (; dev != devices.end(); ++dev) {
if (content::IsAudioMediaType(dev->type)) {
--iter->audio_ref_count;
} else if (content::IsVideoMediaType(dev->type)) {
--iter->video_ref_count;
} else {
NOTIMPLEMENTED();
}
DCHECK_GE(iter->audio_ref_count, 0);
DCHECK_GE(iter->video_ref_count, 0);
}
// Remove the tab if all the devices have been closed.
if (iter->audio_ref_count == 0 && iter->video_ref_count == 0)
tabs_.erase(iter);
}
UpdateStatusTrayIconContextMenu();
}
void MediaStreamCaptureIndicator::EnsureImageLoadingTracker() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (tracker_.get())
return;
tracker_.reset(new ImageLoadingTracker(this));
pending_messages_.clear();
request_index_ = 0;
}