blob: 794de17e59bee5c208f65df8512a149dd6c868c2 [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/content/content_proxy.h"
#include "athena/content/public/dialogs.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/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(views::WebView* web_view)
: web_view_(web_view), reserved_accelerator_enabled_(true) {}
virtual ~WebActivityController() {}
// 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:
virtual 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;
}
virtual 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:
web_view_->GetWidget()->Close();
return true;
case CMD_STOP:
web_view_->GetWebContents()->Stop();
return true;
}
return false;
}
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 SkColor kDefaultUnavailableColor = SkColorSetRGB(0xbb, 0x77, 0x77);
const int kIconSize = 32;
const int kDistanceShowReloadMessage = 100;
const int 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:
AthenaWebView(content::BrowserContext* context)
: views::WebView(context), controller_(new WebActivityController(this)),
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)
: views::WebView(web_contents->GetBrowserContext()),
controller_(new WebActivityController(this)) {
scoped_ptr<content::WebContents> old_contents(
SwapWebContents(scoped_ptr<content::WebContents>(web_contents)));
}
virtual ~AthenaWebView() {}
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 ReloadContent() {
CHECK(evicted_web_contents_.get());
scoped_ptr<content::WebContents> replaced_contents(SwapWebContents(
evicted_web_contents_.Pass()));
}
// Check if the content got evicted.
const bool IsContentEvicted() { return !!evicted_web_contents_.get(); }
// content::WebContentsDelegate:
virtual 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;
}
// NULL is returned if the URL wasn't opened immediately.
return NULL;
}
virtual bool CanOverscrollContent() const OVERRIDE {
const std::string value = CommandLine::ForCurrentProcess()->
GetSwitchValueASCII(switches::kOverscrollHistoryNavigation);
return value != "0";
}
virtual void OverscrollUpdate(int 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) /
static_cast<float>(kDistanceReload - kDistanceShowReloadMessage);
}
reload_message_->GetLayer()->SetOpacity(opacity);
} else if (reload_message_) {
reload_message_->Hide();
}
}
virtual void OverscrollComplete() OVERRIDE {
if (overscroll_y_ >= kDistanceReload)
GetWebContents()->GetController().Reload(false);
if (reload_message_)
reload_message_->Hide();
overscroll_y_ = 0;
}
virtual void AddNewContents(content::WebContents* source,
content::WebContents* new_contents,
WindowOpenDisposition disposition,
const gfx::Rect& initial_pos,
bool user_gesture,
bool* was_blocked) OVERRIDE {
// TODO(oshima): Use factory.
ActivityManager::Get()->AddActivity(
new WebActivity(new AthenaWebView(new_contents)));
}
virtual bool PreHandleKeyboardEvent(
content::WebContents* source,
const content::NativeWebKeyboardEvent& event,
bool* is_keyboard_shortcut) OVERRIDE {
return controller_->PreHandleKeyboardEvent(
source, event, is_keyboard_shortcut);
}
virtual void HandleKeyboardEvent(
content::WebContents* source,
const content::NativeWebKeyboardEvent& event) OVERRIDE {
controller_->HandleKeyboardEvent(source, event);
}
virtual void ToggleFullscreenModeForTab(content::WebContents* web_contents,
bool enter_fullscreen) OVERRIDE {
fullscreen_ = enter_fullscreen;
GetWidget()->SetFullscreen(fullscreen_);
}
virtual bool IsFullscreenForTabOrPending(
const content::WebContents* web_contents) const OVERRIDE {
return fullscreen_;
}
virtual void LoadingStateChanged(content::WebContents* source,
bool to_different_document) OVERRIDE {
bool has_stopped = source == NULL || !source->IsLoading();
LoadProgressChanged(source, has_stopped ? 1 : 0);
}
virtual 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);
}
virtual 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.
virtual void RunFileChooser(
content::WebContents* web_contents,
const content::FileChooserParams& params) OVERRIDE {
return athena::OpenFileChooser(web_contents, params);
}
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_;
// 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.
int overscroll_y_;
DISALLOW_COPY_AND_ASSIGN(AthenaWebView);
};
WebActivity::WebActivity(content::BrowserContext* browser_context,
const base::string16& title,
const GURL& url)
: browser_context_(browser_context),
title_(title),
url_(url),
web_view_(NULL),
title_color_(kDefaultTitleColor),
current_state_(ACTIVITY_UNLOADED),
weak_ptr_factory_(this) {
}
WebActivity::WebActivity(AthenaWebView* web_view)
: browser_context_(web_view->browser_context()),
url_(web_view->GetWebContents()->GetURL()),
web_view_(web_view),
current_state_(ACTIVITY_UNLOADED),
weak_ptr_factory_(this) {
// Transition to state ACTIVITY_INVISIBLE to perform the same setup steps
// as on new activities (namely adding a WebContentsObserver).
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_);
switch (state) {
case ACTIVITY_VISIBLE:
if (!web_view_)
break;
HideContentProxy();
ReloadAndObserve();
break;
case ACTIVITY_INVISIBLE:
if (!web_view_)
break;
if (current_state_ == ACTIVITY_VISIBLE)
ShowContentProxy();
else
ReloadAndObserve();
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(NULL);
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_ ||
!web_view_->IsContentEvicted() ||
current_state_ == ACTIVITY_UNLOADED);
return current_state_;
}
bool WebActivity::IsVisible() {
return web_view_ &&
web_view_->visible() &&
current_state_ != ACTIVITY_UNLOADED;
}
Activity::ActivityMediaState WebActivity::GetMediaState() {
// TODO(skuhne): The function GetTabMediaStateForContents(WebContents),
// and the AudioStreamMonitor needs to be moved from Chrome into contents to
// make it more modular and so that we can use it from here.
return Activity::ACTIVITY_MEDIA_STATE_NONE;
}
aura::Window* WebActivity::GetWindow() {
return !web_view_ ? NULL : web_view_->GetWidget()->GetNativeWindow();
}
content::WebContents* WebActivity::GetWebContents() {
return !web_view_ ? NULL : web_view_->GetWebContents();
}
void WebActivity::Init() {
DCHECK(web_view_);
web_view_->InstallAccelerators();
}
SkColor WebActivity::GetRepresentativeColor() const {
return web_view_ ? title_color_ : kDefaultUnavailableColor;
}
base::string16 WebActivity::GetTitle() const {
if (!title_.empty())
return title_;
// TODO(oshima): Use title set by the web contents.
return web_view_ ? base::UTF8ToUTF16(
web_view_->GetWebContents()->GetVisibleURL().host())
: base::string16();
}
gfx::ImageSkia WebActivity::GetIcon() const {
return icon_;
}
bool WebActivity::UsesFrame() const {
return true;
}
views::View* WebActivity::GetContentsView() {
if (!web_view_) {
web_view_ = new AthenaWebView(browser_context_);
web_view_->LoadInitialURL(url_);
// Make sure the content gets properly shown.
if (current_state_ == ACTIVITY_VISIBLE) {
HideContentProxy();
ReloadAndObserve();
} else if (current_state_ == ACTIVITY_INVISIBLE) {
ShowContentProxy();
ReloadAndObserve();
} else {
// If not previously specified, we change the state now to invisible..
SetCurrentState(ACTIVITY_INVISIBLE);
}
}
return web_view_;
}
views::Widget* WebActivity::CreateWidget() {
return NULL; // Use default widget.
}
gfx::ImageSkia WebActivity::GetOverviewModeImage() {
if (content_proxy_.get())
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) {
ActivityManager::Get()->UpdateActivity(this);
}
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();
ActivityManager::Get()->UpdateActivity(this);
}
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, NULL);
ActivityManager::Get()->UpdateActivity(this);
}
void WebActivity::DidChangeThemeColor(SkColor theme_color) {
title_color_ = theme_color;
ActivityManager::Get()->UpdateActivity(this);
}
void WebActivity::HideContentProxy() {
if (content_proxy_.get())
content_proxy_.reset(NULL);
}
void WebActivity::ShowContentProxy() {
if (!content_proxy_.get() && web_view_)
content_proxy_.reset(new ContentProxy(web_view_, this));
}
void WebActivity::ReloadAndObserve() {
if (web_view_->IsContentEvicted()) {
DCHECK_EQ(ACTIVITY_UNLOADED, current_state_);
web_view_->ReloadContent();
}
Observe(web_view_->GetWebContents());
}
} // namespace athena