blob: d076ca2a5d19fe8280cbe44921011ade09bf2583 [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/ui/extensions/shell_window.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_process_manager.h"
#include "chrome/browser/extensions/extension_system.h"
#include "chrome/browser/extensions/shell_window_geometry_cache.h"
#include "chrome/browser/extensions/shell_window_registry.h"
#include "chrome/browser/extensions/suggest_permission_util.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/favicon/favicon_tab_helper.h"
#include "chrome/browser/file_select_helper.h"
#include "chrome/browser/intents/web_intents_util.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_id.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/constrained_window_tab_helper.h"
#include "chrome/browser/ui/extensions/native_app_window.h"
#include "chrome/browser/ui/intents/web_intent_picker_controller.h"
#include "chrome/browser/view_type_utils.h"
#include "chrome/common/chrome_notification_types.h"
#include "chrome/common/extensions/api/app_window.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/extension_messages.h"
#include "chrome/common/extensions/request_media_access_permission_helper.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/invalidate_type.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/resource_dispatcher_host.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_intents_dispatcher.h"
#include "content/public/common/media_stream_request.h"
#include "content/public/common/renderer_preferences.h"
#include "third_party/skia/include/core/SkRegion.h"
namespace app_window = extensions::api::app_window;
using content::BrowserThread;
using content::ConsoleMessageLevel;
using content::RenderViewHost;
using content::ResourceDispatcherHost;
using content::SiteInstance;
using content::WebContents;
using extensions::APIPermission;
using extensions::RequestMediaAccessPermissionHelper;
namespace {
const int kDefaultWidth = 512;
const int kDefaultHeight = 384;
void SuspendRenderViewHost(RenderViewHost* rvh) {
DCHECK(rvh);
BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
base::Bind(&ResourceDispatcherHost::BlockRequestsForRoute,
base::Unretained(ResourceDispatcherHost::Get()),
rvh->GetProcess()->GetID(), rvh->GetRoutingID()));
}
} // namespace
ShellWindow::CreateParams::CreateParams()
: window_type(ShellWindow::WINDOW_TYPE_DEFAULT),
frame(ShellWindow::FRAME_CHROME),
bounds(INT_MIN, INT_MIN, INT_MIN, INT_MIN),
creator_process_id(0), hidden(false) {
}
ShellWindow::CreateParams::~CreateParams() {
}
ShellWindow* ShellWindow::Create(Profile* profile,
const extensions::Extension* extension,
const GURL& url,
const CreateParams& params) {
// This object will delete itself when the window is closed.
ShellWindow* window = new ShellWindow(profile, extension);
window->Init(url, params);
extensions::ShellWindowRegistry::Get(profile)->AddShellWindow(window);
return window;
}
void ShellWindow::SetAppIconUrl(const GURL& url) {
app_icon_url_ = url;
extensions::ShellWindowRegistry::Get(profile_)->ShellWindowIconChanged(this);
}
ShellWindow::ShellWindow(Profile* profile,
const extensions::Extension* extension)
: profile_(profile),
extension_(extension),
web_contents_(NULL),
window_type_(WINDOW_TYPE_DEFAULT),
ALLOW_THIS_IN_INITIALIZER_LIST(
extension_function_dispatcher_(profile, this)) {
}
void ShellWindow::Init(const GURL& url,
const ShellWindow::CreateParams& params) {
window_type_ = params.window_type;
web_contents_.reset(WebContents::Create(
profile(), SiteInstance::CreateForURL(profile(), url), MSG_ROUTING_NONE,
NULL));
ConstrainedWindowTabHelper::CreateForWebContents(web_contents_.get());
FaviconTabHelper::CreateForWebContents(web_contents_.get());
WebIntentPickerController::CreateForWebContents(web_contents_.get());
content::WebContentsObserver::Observe(web_contents_.get());
web_contents_->SetDelegate(this);
chrome::SetViewType(web_contents_.get(), chrome::VIEW_TYPE_APP_SHELL);
web_contents_->GetMutableRendererPrefs()->
browser_handles_all_top_level_requests = true;
web_contents_->GetRenderViewHost()->SyncRendererPrefs();
gfx::Rect bounds = params.bounds;
if (bounds.width() == INT_MIN)
bounds.set_width(kDefaultWidth);
if (bounds.height() == INT_MIN)
bounds.set_height(kDefaultHeight);
// If left and top are left undefined, the native shell window will center
// the window on the main screen in a platform-defined manner.
if (!params.window_key.empty()) {
window_key_ = params.window_key;
extensions::ShellWindowGeometryCache* cache =
extensions::ExtensionSystem::Get(profile())->
shell_window_geometry_cache();
gfx::Rect cached_bounds;
if (cache->GetGeometry(extension()->id(), params.window_key,
&cached_bounds))
bounds = cached_bounds;
}
ShellWindow::CreateParams new_params = params;
gfx::Size& minimum_size = new_params.minimum_size;
gfx::Size& maximum_size = new_params.maximum_size;
// In the case that minimum size > maximum size, we consider the minimum
// size to be more important.
if (maximum_size.width() && maximum_size.width() < minimum_size.width())
maximum_size.set_width(minimum_size.width());
if (maximum_size.height() && maximum_size.height() < minimum_size.height())
maximum_size.set_height(minimum_size.height());
if (maximum_size.width() && bounds.width() > maximum_size.width())
bounds.set_width(maximum_size.width());
if (bounds.width() != INT_MIN && bounds.width() < minimum_size.width())
bounds.set_width(minimum_size.width());
if (maximum_size.height() && bounds.height() > maximum_size.height())
bounds.set_height(maximum_size.height());
if (bounds.height() != INT_MIN && bounds.height() < minimum_size.height())
bounds.set_height(minimum_size.height());
new_params.bounds = bounds;
native_app_window_.reset(NativeAppWindow::Create(this, new_params));
OnNativeWindowChanged();
if (!params.hidden) {
if (params.window_type == WINDOW_TYPE_PANEL)
GetBaseWindow()->ShowInactive(); // Panels are not activated by default.
else
GetBaseWindow()->Show();
}
// If the new view is in the same process as the creator, block the created
// RVH from loading anything until the background page has had a chance to do
// any initialization it wants. If it's a different process, the new RVH
// shouldn't communicate with the background page anyway (e.g. sandboxed).
if (web_contents_->GetRenderViewHost()->GetProcess()->GetID() ==
params.creator_process_id) {
SuspendRenderViewHost(web_contents_->GetRenderViewHost());
} else {
VLOG(1) << "ShellWindow created in new process ("
<< web_contents_->GetRenderViewHost()->GetProcess()->GetID()
<< ") != creator (" << params.creator_process_id
<< "). Routing disabled.";
}
// TODO(jeremya): there's a bug where navigating a web contents to an
// extension URL causes it to create a new RVH and discard the old (perfectly
// usable) one. To work around this, we watch for a RVH_CHANGED message from
// the web contents (which will be sent during LoadURL) and suspend resource
// requests on the new RVH to ensure that we block the new RVH from loading
// anything. It should be okay to remove the NOTIFICATION_RVH_CHANGED
// registration once http://crbug.com/123007 is fixed.
registrar_.Add(this, content::NOTIFICATION_RENDER_VIEW_HOST_CHANGED,
content::Source<content::NavigationController>(
&web_contents_->GetController()));
web_contents_->GetController().LoadURL(
url, content::Referrer(), content::PAGE_TRANSITION_LINK,
std::string());
registrar_.RemoveAll();
registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED,
content::Source<Profile>(profile_));
// Close when the browser is exiting.
// TODO(mihaip): we probably don't want this in the long run (when platform
// apps are no longer tied to the browser process).
registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING,
content::NotificationService::AllSources());
// Prevent the browser process from shutting down while this window is open.
browser::StartKeepAlive();
UpdateExtensionAppIcon();
}
ShellWindow::~ShellWindow() {
// Unregister now to prevent getting NOTIFICATION_APP_TERMINATING if we're the
// last window open.
registrar_.RemoveAll();
// Remove shutdown prevention.
browser::EndKeepAlive();
}
void ShellWindow::RequestMediaAccessPermission(
content::WebContents* web_contents,
const content::MediaStreamRequest* request,
const content::MediaResponseCallback& callback) {
RequestMediaAccessPermissionHelper::AuthorizeRequest(
request, callback, extension(), true);
}
WebContents* ShellWindow::OpenURLFromTab(WebContents* source,
const content::OpenURLParams& params) {
DCHECK(source == web_contents_);
// Don't allow the current tab to be navigated. It would be nice to map all
// anchor tags (even those without target="_blank") to new tabs, but right
// now we can't distinguish between those and <meta> refreshes or window.href
// navigations, which we don't want to allow.
// TOOD(mihaip): Can we check for user gestures instead?
WindowOpenDisposition disposition = params.disposition;
if (disposition == CURRENT_TAB) {
AddMessageToDevToolsConsole(
content::CONSOLE_MESSAGE_LEVEL_ERROR,
base::StringPrintf(
"Can't open same-window link to \"%s\"; try target=\"_blank\".",
params.url.spec().c_str()));
return NULL;
}
// These dispositions aren't really navigations.
if (disposition == SUPPRESS_OPEN || disposition == SAVE_TO_DISK ||
disposition == IGNORE_ACTION) {
return NULL;
}
// Force all links to open in a new tab, even if they were trying to open a
// window.
chrome::NavigateParams new_tab_params(
static_cast<Browser*>(NULL), params.url, params.transition);
new_tab_params.disposition =
disposition == NEW_BACKGROUND_TAB ? disposition : NEW_FOREGROUND_TAB;
new_tab_params.initiating_profile = profile_;
chrome::Navigate(&new_tab_params);
if (!new_tab_params.target_contents) {
AddMessageToDevToolsConsole(
content::CONSOLE_MESSAGE_LEVEL_ERROR,
base::StringPrintf(
"Can't navigate to \"%s\"; apps do not support navigation.",
params.url.spec().c_str()));
}
return new_tab_params.target_contents;
}
void ShellWindow::AddNewContents(WebContents* source,
WebContents* new_contents,
WindowOpenDisposition disposition,
const gfx::Rect& initial_pos,
bool user_gesture,
bool* was_blocked) {
DCHECK(source == web_contents_);
DCHECK(Profile::FromBrowserContext(new_contents->GetBrowserContext()) ==
profile_);
Browser* browser =
browser::FindOrCreateTabbedBrowser(profile_, chrome::GetActiveDesktop());
// Force all links to open in a new tab, even if they were trying to open a
// new window.
disposition =
disposition == NEW_BACKGROUND_TAB ? disposition : NEW_FOREGROUND_TAB;
chrome::AddWebContents(browser, NULL, new_contents, disposition, initial_pos,
user_gesture, was_blocked);
}
void ShellWindow::HandleKeyboardEvent(
WebContents* source,
const content::NativeWebKeyboardEvent& event) {
DCHECK_EQ(source, web_contents_);
native_app_window_->HandleKeyboardEvent(event);
}
void ShellWindow::RequestToLockMouse(WebContents* web_contents,
bool user_gesture,
bool last_unlocked_by_target) {
bool has_permission = IsExtensionWithPermissionOrSuggestInConsole(
APIPermission::kPointerLock,
extension_,
web_contents->GetRenderViewHost());
web_contents->GotResponseToLockMouseRequest(has_permission);
}
void ShellWindow::OnNativeClose() {
extensions::ShellWindowRegistry::Get(profile_)->RemoveShellWindow(this);
content::RenderViewHost* rvh = web_contents_->GetRenderViewHost();
rvh->Send(new ExtensionMsg_AppWindowClosed(rvh->GetRoutingID()));
delete this;
}
void ShellWindow::OnNativeWindowChanged() {
SaveWindowPosition();
if (!native_app_window_ || !web_contents_)
return;
ListValue args;
DictionaryValue* dictionary = new DictionaryValue();
args.Append(dictionary);
gfx::Rect bounds = native_app_window_->GetBounds();
app_window::Bounds update;
update.left.reset(new int(bounds.x()));
update.top.reset(new int(bounds.y()));
update.width.reset(new int(bounds.width()));
update.height.reset(new int(bounds.height()));
dictionary->Set("bounds", update.ToValue().release());
dictionary->SetBoolean("minimized", native_app_window_->IsMinimized());
dictionary->SetBoolean("maximized", native_app_window_->IsMaximized());
content::RenderViewHost* rvh = web_contents_->GetRenderViewHost();
rvh->Send(new ExtensionMsg_MessageInvoke(rvh->GetRoutingID(),
extension_->id(),
"updateAppWindowProperties",
args,
GURL(),
false));
}
NativeAppWindow* ShellWindow::GetBaseWindow() {
return native_app_window_.get();
}
gfx::NativeWindow ShellWindow::GetNativeWindow() {
return GetBaseWindow()->GetNativeWindow();
}
string16 ShellWindow::GetTitle() const {
// WebContents::GetTitle() will return the page's URL if there's no <title>
// specified. However, we'd prefer to show the name of the extension in that
// case, so we directly inspect the NavigationEntry's title.
if (!web_contents() ||
!web_contents()->GetController().GetActiveEntry() ||
web_contents()->GetController().GetActiveEntry()->GetTitle().empty())
return UTF8ToUTF16(extension()->name());
string16 title = web_contents()->GetTitle();
Browser::FormatTitleForDisplay(&title);
return title;
}
bool ShellWindow::OnMessageReceived(const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(ShellWindow, message)
IPC_MESSAGE_HANDLER(ExtensionHostMsg_Request, OnRequest)
IPC_MESSAGE_HANDLER(ExtensionHostMsg_UpdateDraggableRegions,
UpdateDraggableRegions)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
void ShellWindow::UpdateDraggableRegions(
const std::vector<extensions::DraggableRegion>& regions) {
native_app_window_->UpdateDraggableRegions(regions);
}
void ShellWindow::OnImageLoaded(const gfx::Image& image,
const std::string& extension_id,
int index) {
if (!image.IsEmpty()) {
app_icon_ = image;
native_app_window_->UpdateWindowIcon();
}
app_icon_loader_.reset();
}
void ShellWindow::UpdateExtensionAppIcon() {
app_icon_loader_.reset(new ImageLoadingTracker(this));
app_icon_loader_->LoadImage(
extension(),
extension()->GetIconResource(extension_misc::EXTENSION_ICON_SMALL,
ExtensionIconSet::MATCH_BIGGER),
gfx::Size(extension_misc::EXTENSION_ICON_SMALL,
extension_misc::EXTENSION_ICON_SMALL),
ImageLoadingTracker::CACHE);
}
void ShellWindow::CloseContents(WebContents* contents) {
DCHECK(contents == web_contents_);
native_app_window_->Close();
}
bool ShellWindow::ShouldSuppressDialogs() {
return true;
}
void ShellWindow::WebIntentDispatch(
content::WebContents* web_contents,
content::WebIntentsDispatcher* intents_dispatcher) {
if (!web_intents::IsWebIntentsEnabledForProfile(profile_))
return;
WebIntentPickerController* web_intent_picker_controller =
WebIntentPickerController::FromWebContents(web_contents_.get());
web_intent_picker_controller->SetIntentsDispatcher(intents_dispatcher);
web_intent_picker_controller->ShowDialog(
intents_dispatcher->GetIntent().action,
intents_dispatcher->GetIntent().type);
}
void ShellWindow::RunFileChooser(WebContents* tab,
const content::FileChooserParams& params) {
FileSelectHelper::RunFileChooser(tab, params);
}
bool ShellWindow::IsPopupOrPanel(const WebContents* source) const {
DCHECK(source == web_contents_.get());
return true;
}
void ShellWindow::MoveContents(WebContents* source, const gfx::Rect& pos) {
DCHECK(source == web_contents_.get());
native_app_window_->SetBounds(pos);
}
void ShellWindow::NavigationStateChanged(
const content::WebContents* source, unsigned changed_flags) {
DCHECK(source == web_contents_.get());
if (changed_flags & content::INVALIDATE_TYPE_TITLE)
native_app_window_->UpdateWindowTitle();
else if (changed_flags & content::INVALIDATE_TYPE_TAB)
native_app_window_->UpdateWindowIcon();
}
void ShellWindow::ToggleFullscreenModeForTab(content::WebContents* source,
bool enter_fullscreen) {
DCHECK(source == web_contents_.get());
native_app_window_->SetFullscreen(enter_fullscreen);
}
bool ShellWindow::IsFullscreenForTabOrPending(
const content::WebContents* source) const {
DCHECK(source == web_contents_.get());
return native_app_window_->IsFullscreenOrPending();
}
void ShellWindow::Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
switch (type) {
case content::NOTIFICATION_RENDER_VIEW_HOST_CHANGED: {
// TODO(jeremya): once http://crbug.com/123007 is fixed, we'll no longer
// need to suspend resource requests here (the call in the constructor
// should be enough).
content::Details<std::pair<RenderViewHost*, RenderViewHost*> >
host_details(details);
if (host_details->first)
SuspendRenderViewHost(host_details->second);
// TODO(jianli): once http://crbug.com/123007 is fixed, we'll no longer
// need to make the native window (ShellWindowViews specially) update
// the clickthrough region for the new RVH.
native_app_window_->RenderViewHostChanged();
break;
}
case chrome::NOTIFICATION_EXTENSION_UNLOADED: {
const extensions::Extension* unloaded_extension =
content::Details<extensions::UnloadedExtensionInfo>(
details)->extension;
if (extension_ == unloaded_extension)
native_app_window_->Close();
break;
}
case chrome::NOTIFICATION_APP_TERMINATING:
native_app_window_->Close();
break;
default:
NOTREACHED() << "Received unexpected notification";
}
}
extensions::WindowController*
ShellWindow::GetExtensionWindowController() const {
return NULL;
}
content::WebContents* ShellWindow::GetAssociatedWebContents() const {
return web_contents_.get();
}
extensions::ActiveTabPermissionGranter*
ShellWindow::GetActiveTabPermissionGranter() {
// Shell windows don't support the activeTab permission.
return NULL;
}
void ShellWindow::OnRequest(const ExtensionHostMsg_Request_Params& params) {
extension_function_dispatcher_.Dispatch(params,
web_contents_->GetRenderViewHost());
}
void ShellWindow::AddMessageToDevToolsConsole(ConsoleMessageLevel level,
const std::string& message) {
content::RenderViewHost* rvh = web_contents_->GetRenderViewHost();
rvh->Send(new ExtensionMsg_AddMessageToConsole(
rvh->GetRoutingID(), level, message));
}
void ShellWindow::SaveWindowPosition() {
if (window_key_.empty())
return;
if (!native_app_window_)
return;
extensions::ShellWindowGeometryCache* cache =
extensions::ExtensionSystem::Get(profile())->
shell_window_geometry_cache();
gfx::Rect bounds = native_app_window_->GetBounds();
cache->SaveGeometry(extension()->id(), window_key_, bounds);
}
// static
SkRegion* ShellWindow::RawDraggableRegionsToSkRegion(
const std::vector<extensions::DraggableRegion>& regions) {
SkRegion* sk_region = new SkRegion;
for (std::vector<extensions::DraggableRegion>::const_iterator iter =
regions.begin();
iter != regions.end(); ++iter) {
const extensions::DraggableRegion& region = *iter;
sk_region->op(
region.bounds.x(),
region.bounds.y(),
region.bounds.right(),
region.bounds.bottom(),
region.draggable ? SkRegion::kUnion_Op : SkRegion::kDifference_Op);
}
return sk_region;
}