blob: a6bdb82b51a516046566685066eb1c40b0b0422f [file] [log] [blame]
// Copyright 2014 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 "athena/content/web_activity.h"
#include "athena/activity/public/activity_factory.h"
#include "athena/activity/public/activity_manager.h"
#include "athena/activity/public/activity_view.h"
#include "athena/content/content_proxy.h"
#include "athena/content/media_utils.h"
#include "athena/content/public/dialogs.h"
#include "athena/content/web_activity_helpers.h"
#include "athena/input/public/accelerator_manager.h"
#include "athena/strings/grit/athena_strings.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/strings/utf_string_conversions.h"
#include "components/app_modal_dialogs/javascript_dialog_manager.h"
#include "components/favicon_base/select_favicon_frames.h"
#include "content/public/browser/native_web_keyboard_event.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/favicon_url.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/closure_animation_observer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/views/background.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/webview/unhandled_keyboard_event_handler.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/widget/widget.h"
namespace athena {
namespace {
class WebActivityController : public AcceleratorHandler {
public:
enum Command {
CMD_BACK,
CMD_FORWARD,
CMD_RELOAD,
CMD_RELOAD_IGNORE_CACHE,
CMD_CLOSE,
CMD_STOP,
};
explicit WebActivityController(WebActivity* owner_activity,
views::WebView* web_view)
: owner_activity_(owner_activity),
web_view_(web_view),
reserved_accelerator_enabled_(true) {}
~WebActivityController() override {}
// Installs accelerators for web activity.
void InstallAccelerators() {
accelerator_manager_ = AcceleratorManager::CreateForFocusManager(
web_view_->GetFocusManager()).Pass();
const AcceleratorData accelerator_data[] = {
{TRIGGER_ON_PRESS, ui::VKEY_R, ui::EF_CONTROL_DOWN, CMD_RELOAD,
AF_NONE},
{TRIGGER_ON_PRESS, ui::VKEY_BROWSER_REFRESH, ui::EF_NONE, CMD_RELOAD,
AF_NONE},
{TRIGGER_ON_PRESS, ui::VKEY_BROWSER_REFRESH, ui::EF_CONTROL_DOWN,
CMD_RELOAD_IGNORE_CACHE, AF_NONE},
{TRIGGER_ON_PRESS, ui::VKEY_BROWSER_FORWARD, ui::EF_NONE, CMD_FORWARD,
AF_NONE},
{TRIGGER_ON_PRESS, ui::VKEY_BROWSER_BACK, ui::EF_NONE, CMD_BACK,
AF_NONE},
{TRIGGER_ON_PRESS, ui::VKEY_W, ui::EF_CONTROL_DOWN, CMD_CLOSE, AF_NONE},
{TRIGGER_ON_PRESS, ui::VKEY_ESCAPE, ui::EF_NONE, CMD_STOP, AF_NONE},
};
accelerator_manager_->RegisterAccelerators(
accelerator_data, arraysize(accelerator_data), this);
}
// Methods that are called before and after key events are consumed by the web
// contents.
// See the documentation in WebContentsDelegate: for more details.
bool PreHandleKeyboardEvent(content::WebContents* source,
const content::NativeWebKeyboardEvent& event,
bool* is_keyboard_shortcut) {
ui::Accelerator accelerator(
static_cast<ui::KeyboardCode>(event.windowsKeyCode),
content::GetModifiersFromNativeWebKeyboardEvent(event));
if (event.type == blink::WebInputEvent::KeyUp)
accelerator.set_type(ui::ET_KEY_RELEASED);
if (reserved_accelerator_enabled_ &&
accelerator_manager_->IsRegistered(accelerator, AF_RESERVED)) {
return web_view_->GetFocusManager()->ProcessAccelerator(accelerator);
}
*is_keyboard_shortcut =
accelerator_manager_->IsRegistered(accelerator, AF_NONE);
return false;
}
void HandleKeyboardEvent(content::WebContents* source,
const content::NativeWebKeyboardEvent& event) {
unhandled_keyboard_event_handler_.HandleKeyboardEvent(
event, web_view_->GetFocusManager());
}
private:
// AcceleratorHandler:
bool IsCommandEnabled(int command_id) const override {
switch (command_id) {
case CMD_RELOAD:
case CMD_RELOAD_IGNORE_CACHE:
return true;
case CMD_BACK:
return web_view_->GetWebContents()->GetController().CanGoBack();
case CMD_FORWARD:
return web_view_->GetWebContents()->GetController().CanGoForward();
case CMD_CLOSE:
// TODO(oshima): check onbeforeunload handler.
return true;
case CMD_STOP:
return web_view_->GetWebContents()->IsLoading();
}
return false;
}
bool OnAcceleratorFired(int command_id,
const ui::Accelerator& accelerator) override {
switch (command_id) {
case CMD_RELOAD:
web_view_->GetWebContents()->GetController().Reload(false);
return true;
case CMD_RELOAD_IGNORE_CACHE:
web_view_->GetWebContents()->GetController().ReloadIgnoringCache(false);
return true;
case CMD_BACK:
web_view_->GetWebContents()->GetController().GoBack();
return true;
case CMD_FORWARD:
web_view_->GetWebContents()->GetController().GoForward();
return true;
case CMD_CLOSE:
Activity::Delete(owner_activity_);
return true;
case CMD_STOP:
web_view_->GetWebContents()->Stop();
return true;
}
return false;
}
Activity* const owner_activity_;
views::WebView* web_view_;
bool reserved_accelerator_enabled_;
scoped_ptr<AcceleratorManager> accelerator_manager_;
views::UnhandledKeyboardEventHandler unhandled_keyboard_event_handler_;
DISALLOW_COPY_AND_ASSIGN(WebActivityController);
};
const SkColor kDefaultTitleColor = SkColorSetRGB(0xf2, 0xf2, 0xf2);
const int kIconSize = 32;
const float kDistanceShowReloadMessage = 100;
const float kDistanceReload = 150;
} // namespace
// A web view for athena's web activity. Note that AthenaWebView will create its
// own content so that it can eject and reload it.
class AthenaWebView : public views::WebView {
public:
explicit AthenaWebView(content::BrowserContext* context,
WebActivity* owner_activity)
: views::WebView(context),
controller_(new WebActivityController(owner_activity, this)),
owner_activity_(owner_activity),
fullscreen_(false),
overscroll_y_(0) {
SetEmbedFullscreenWidgetMode(true);
// TODO(skuhne): Add content observer to detect renderer crash and set
// content status to unloaded if that happens.
}
AthenaWebView(content::WebContents* web_contents,
WebActivity* owner_activity)
: views::WebView(web_contents->GetBrowserContext()),
controller_(new WebActivityController(owner_activity, this)),
owner_activity_(owner_activity) {
scoped_ptr<content::WebContents> old_contents(
SwapWebContents(scoped_ptr<content::WebContents>(web_contents)));
}
~AthenaWebView() override {}
void InstallAccelerators() { controller_->InstallAccelerators(); }
void EvictContent() {
scoped_ptr<content::WebContents> old_contents(SwapWebContents(
scoped_ptr<content::WebContents>(content::WebContents::Create(
content::WebContents::CreateParams(browser_context())))));
// If there is a progress bar, we need to get rid of it now since its
// associated content, parent window and layers will disappear with evicting
// the content.
progress_bar_.reset();
evicted_web_contents_.reset(
content::WebContents::Create(content::WebContents::CreateParams(
old_contents->GetBrowserContext())));
evicted_web_contents_->GetController().CopyStateFrom(
old_contents->GetController());
// As soon as the new contents becomes visible, it should reload.
// TODO(skuhne): This breaks script connections with other activities.
// Even though this is the same technique as used by the TabStripModel,
// we might want to address this cleaner since we are more likely to
// run into this state. by unloading.
}
void AttachHelpers() {
if (!IsContentEvicted())
AttachWebActivityHelpers(GetWebContents());
// Else: The helpers will be attached when the evicted content is reloaded.
}
void ReloadEvictedContent() {
CHECK(evicted_web_contents_.get());
// Order is important. The helpers must be attached prior to the RenderView
// being created.
AttachWebActivityHelpers(evicted_web_contents_.get());
SwapWebContents(evicted_web_contents_.Pass());
}
// Check if the content got evicted.
const bool IsContentEvicted() { return !!evicted_web_contents_.get(); }
// content::WebContentsDelegate:
content::WebContents* OpenURLFromTab(
content::WebContents* source,
const content::OpenURLParams& params) override {
switch(params.disposition) {
case CURRENT_TAB: {
DCHECK(source == web_contents());
content::NavigationController::LoadURLParams load_url_params(
params.url);
load_url_params.referrer = params.referrer;
load_url_params.frame_tree_node_id = params.frame_tree_node_id;
load_url_params.transition_type = params.transition;
load_url_params.extra_headers = params.extra_headers;
load_url_params.should_replace_current_entry =
params.should_replace_current_entry;
load_url_params.is_renderer_initiated = params.is_renderer_initiated;
load_url_params.transferred_global_request_id =
params.transferred_global_request_id;
web_contents()->GetController().LoadURLWithParams(load_url_params);
return web_contents();
}
case NEW_FOREGROUND_TAB:
case NEW_BACKGROUND_TAB:
case NEW_POPUP:
case NEW_WINDOW: {
Activity* activity = ActivityFactory::Get()->CreateWebActivity(
browser_context(), base::string16(), params.url);
Activity::Show(activity);
break;
}
default:
break;
}
// nullptr is returned if the URL wasn't opened immediately.
return nullptr;
}
bool CanOverscrollContent() const override {
const std::string value = CommandLine::ForCurrentProcess()->
GetSwitchValueASCII(switches::kOverscrollHistoryNavigation);
return value != "0";
}
void OverscrollUpdate(float delta_y) override {
overscroll_y_ = delta_y;
if (overscroll_y_ > kDistanceShowReloadMessage) {
if (!reload_message_)
CreateReloadMessage();
reload_message_->Show();
float opacity = 1.0f;
if (overscroll_y_ < kDistanceReload) {
opacity = (overscroll_y_ - kDistanceShowReloadMessage) /
(kDistanceReload - kDistanceShowReloadMessage);
}
reload_message_->GetLayer()->SetOpacity(opacity);
} else if (reload_message_) {
reload_message_->Hide();
}
}
void OverscrollComplete() override {
if (overscroll_y_ >= kDistanceReload)
GetWebContents()->GetController().Reload(false);
if (reload_message_)
reload_message_->Hide();
overscroll_y_ = 0;
}
void AddNewContents(content::WebContents* source,
content::WebContents* new_contents,
WindowOpenDisposition disposition,
const gfx::Rect& initial_pos,
bool user_gesture,
bool* was_blocked) override {
Activity* activity =
ActivityFactory::Get()->CreateWebActivity(new_contents);
Activity::Show(activity);
}
bool PreHandleKeyboardEvent(content::WebContents* source,
const content::NativeWebKeyboardEvent& event,
bool* is_keyboard_shortcut) override {
return controller_->PreHandleKeyboardEvent(
source, event, is_keyboard_shortcut);
}
void HandleKeyboardEvent(
content::WebContents* source,
const content::NativeWebKeyboardEvent& event) override {
controller_->HandleKeyboardEvent(source, event);
}
void ToggleFullscreenModeForTab(content::WebContents* web_contents,
bool enter_fullscreen) override {
fullscreen_ = enter_fullscreen;
GetWidget()->SetFullscreen(fullscreen_);
}
bool IsFullscreenForTabOrPending(
const content::WebContents* web_contents) const override {
return fullscreen_;
}
void LoadingStateChanged(content::WebContents* source,
bool to_different_document) override {
bool has_stopped = source == nullptr || !source->IsLoading();
LoadProgressChanged(source, has_stopped ? 1 : 0);
}
void LoadProgressChanged(content::WebContents* source,
double progress) override {
if (!progress)
return;
if (!progress_bar_) {
CreateProgressBar();
source->GetNativeView()->layer()->Add(progress_bar_.get());
}
progress_bar_->SetBounds(gfx::Rect(
0, 0, progress * progress_bar_->parent()->bounds().width(), 3));
if (progress < 1)
return;
ui::ScopedLayerAnimationSettings settings(progress_bar_->GetAnimator());
settings.SetTweenType(gfx::Tween::EASE_IN);
ui::Layer* layer = progress_bar_.get();
settings.AddObserver(new ui::ClosureAnimationObserver(
base::Bind(&base::DeletePointer<ui::Layer>, progress_bar_.release())));
layer->SetOpacity(0.f);
}
content::JavaScriptDialogManager* GetJavaScriptDialogManager() override {
return GetJavaScriptDialogManagerInstance();
}
content::ColorChooser* OpenColorChooser(
content::WebContents* web_contents,
SkColor color,
const std::vector<content::ColorSuggestion>& suggestions) override {
return athena::OpenColorChooser(web_contents, color, suggestions);
}
// Called when a file selection is to be done.
void RunFileChooser(content::WebContents* web_contents,
const content::FileChooserParams& params) override {
return athena::OpenFileChooser(web_contents, params);
}
void CloseContents(content::WebContents* contents) override {
Activity::Delete(owner_activity_);
}
private:
void CreateProgressBar() {
CHECK(!progress_bar_);
progress_bar_.reset(new ui::Layer(ui::LAYER_SOLID_COLOR));
progress_bar_->SetColor(SkColorSetRGB(0x17, 0x59, 0xcd));
}
void CreateReloadMessage() {
CHECK(!reload_message_);
reload_message_.reset(new views::Widget);
views::Widget::InitParams params(views::Widget::InitParams::TYPE_CONTROL);
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.parent = GetWidget()->GetNativeView();
reload_message_->Init(params);
views::Label* label = new views::Label(
l10n_util::GetStringUTF16(IDS_ATHENA_PULL_TO_RELOAD_MESSAGE));
label->SetBackgroundColor(SK_ColorGRAY);
label->set_background(
views::Background::CreateSolidBackground(SK_ColorGRAY));
reload_message_->SetContentsView(label);
reload_message_->SetBounds(ConvertRectToWidget(
gfx::Rect(0, 0, width(), label->GetPreferredSize().height())));
}
scoped_ptr<WebActivityController> controller_;
Activity* const owner_activity_;
// If the activity got evicted, this is the web content which holds the known
// state of the content before eviction.
scoped_ptr<content::WebContents> evicted_web_contents_;
scoped_ptr<ui::Layer> progress_bar_;
scoped_ptr<views::Widget> reload_message_;
// TODO(oshima): Find out if we should support window fullscreen.
// It may still useful when a user is in split mode.
bool fullscreen_;
// The distance that the user has overscrolled vertically.
float overscroll_y_;
DISALLOW_COPY_AND_ASSIGN(AthenaWebView);
};
WebActivity::WebActivity(content::BrowserContext* browser_context,
const base::string16& title,
const GURL& url)
: browser_context_(browser_context),
web_view_(new AthenaWebView(browser_context, this)),
title_(title),
title_color_(kDefaultTitleColor),
current_state_(ACTIVITY_UNLOADED),
activity_view_(nullptr),
weak_ptr_factory_(this) {
// Order is important. The web activity helpers must be attached prior to the
// RenderView being created.
SetCurrentState(ACTIVITY_INVISIBLE);
web_view_->LoadInitialURL(url);
}
WebActivity::WebActivity(content::WebContents* contents)
: browser_context_(contents->GetBrowserContext()),
web_view_(new AthenaWebView(contents, this)),
title_color_(kDefaultTitleColor),
current_state_(ACTIVITY_UNLOADED),
activity_view_(nullptr),
weak_ptr_factory_(this) {
// If the activity was created as a result of
// WebContentsDelegate::AddNewContents(), web activity helpers may not be
// created prior to the RenderView being created. Desktop Chrome has a
// similar problem.
SetCurrentState(ACTIVITY_INVISIBLE);
}
WebActivity::~WebActivity() {
// It is not required to change the activity state to UNLOADED - unless we
// would add state observers.
}
ActivityViewModel* WebActivity::GetActivityViewModel() {
return this;
}
void WebActivity::SetCurrentState(Activity::ActivityState state) {
DCHECK_NE(state, current_state_);
if (current_state_ == ACTIVITY_UNLOADED) {
web_view_->AttachHelpers();
if (web_view_->IsContentEvicted())
web_view_->ReloadEvictedContent();
Observe(web_view_->GetWebContents());
}
switch (state) {
case ACTIVITY_VISIBLE:
HideContentProxy();
break;
case ACTIVITY_INVISIBLE:
if (current_state_ == ACTIVITY_VISIBLE)
ShowContentProxy();
break;
case ACTIVITY_BACKGROUND_LOW_PRIORITY:
DCHECK(ACTIVITY_VISIBLE == current_state_ ||
ACTIVITY_INVISIBLE == current_state_);
// TODO(skuhne): Do this.
break;
case ACTIVITY_PERSISTENT:
DCHECK_EQ(ACTIVITY_BACKGROUND_LOW_PRIORITY, current_state_);
// TODO(skuhne): Do this. As soon as the new resource management is
// agreed upon - or remove otherwise.
break;
case ACTIVITY_UNLOADED:
DCHECK_NE(ACTIVITY_UNLOADED, current_state_);
if (content_proxy_)
content_proxy_->ContentWillUnload();
Observe(nullptr);
web_view_->EvictContent();
break;
}
// Remember the last requested state.
current_state_ = state;
}
Activity::ActivityState WebActivity::GetCurrentState() {
// If the content is evicted, the state has to be UNLOADED.
DCHECK(!web_view_->IsContentEvicted() ||
current_state_ == ACTIVITY_UNLOADED);
return current_state_;
}
bool WebActivity::IsVisible() {
return web_view_->visible() && current_state_ != ACTIVITY_UNLOADED;
}
Activity::ActivityMediaState WebActivity::GetMediaState() {
return current_state_ == ACTIVITY_UNLOADED ?
Activity::ACTIVITY_MEDIA_STATE_NONE :
GetActivityMediaState(GetWebContents());
}
aura::Window* WebActivity::GetWindow() {
return web_view_->GetWidget() ? web_view_->GetWidget()->GetNativeWindow()
: nullptr;
}
content::WebContents* WebActivity::GetWebContents() {
return web_view_->GetWebContents();
}
void WebActivity::Init() {
web_view_->InstallAccelerators();
}
SkColor WebActivity::GetRepresentativeColor() const {
return title_color_;
}
base::string16 WebActivity::GetTitle() const {
if (!title_.empty())
return title_;
const base::string16& title = web_view_->GetWebContents()->GetTitle();
if (!title.empty())
return title;
return base::UTF8ToUTF16(web_view_->GetWebContents()->GetVisibleURL().host());
}
gfx::ImageSkia WebActivity::GetIcon() const {
return icon_;
}
void WebActivity::SetActivityView(ActivityView* view) {
DCHECK(!activity_view_);
activity_view_ = view;
}
bool WebActivity::UsesFrame() const {
return true;
}
views::View* WebActivity::GetContentsView() {
return web_view_;
}
gfx::ImageSkia WebActivity::GetOverviewModeImage() {
if (content_proxy_.get())
return content_proxy_->GetContentImage();
return gfx::ImageSkia();
}
void WebActivity::PrepareContentsForOverview() {
// Turn on fast resizing to avoid re-laying out the web contents when
// entering / exiting overview mode and the content is visible.
if (!content_proxy_.get())
web_view_->SetFastResize(true);
}
void WebActivity::ResetContentsView() {
// Turn on fast resizing to avoid re-laying out the web contents when
// entering / exiting overview mode and the content is visible.
if (!content_proxy_.get()) {
web_view_->SetFastResize(false);
web_view_->Layout();
}
}
void WebActivity::TitleWasSet(content::NavigationEntry* entry,
bool explicit_set) {
if (activity_view_)
activity_view_->UpdateTitle();
}
void WebActivity::DidNavigateMainFrame(
const content::LoadCommittedDetails& details,
const content::FrameNavigateParams& params) {
// Prevent old image requests from calling back to OnDidDownloadFavicon().
weak_ptr_factory_.InvalidateWeakPtrs();
icon_ = gfx::ImageSkia();
if (activity_view_)
activity_view_->UpdateIcon();
}
void WebActivity::DidUpdateFaviconURL(
const std::vector<content::FaviconURL>& candidates) {
// Pick an arbitrary favicon of type FAVICON to use.
// TODO(pkotwicz): Do something better once the favicon code is componentized.
// (crbug.com/401997)
weak_ptr_factory_.InvalidateWeakPtrs();
for (size_t i = 0; i < candidates.size(); ++i) {
if (candidates[i].icon_type == content::FaviconURL::FAVICON) {
web_view_->GetWebContents()->DownloadImage(
candidates[i].icon_url,
true,
0,
base::Bind(&WebActivity::OnDidDownloadFavicon,
weak_ptr_factory_.GetWeakPtr()));
break;
}
}
}
void WebActivity::OnDidDownloadFavicon(
int id,
int http_status_code,
const GURL& url,
const std::vector<SkBitmap>& bitmaps,
const std::vector<gfx::Size>& original_bitmap_sizes) {
icon_ = CreateFaviconImageSkia(
bitmaps, original_bitmap_sizes, kIconSize, nullptr);
if (activity_view_)
activity_view_->UpdateIcon();
}
void WebActivity::DidChangeThemeColor(SkColor theme_color) {
title_color_ = theme_color;
if (activity_view_)
activity_view_->UpdateRepresentativeColor();
}
void WebActivity::HideContentProxy() {
if (content_proxy_.get())
content_proxy_.reset(nullptr);
}
void WebActivity::ShowContentProxy() {
if (!content_proxy_.get())
content_proxy_.reset(new ContentProxy(web_view_));
}
} // namespace athena