普通文本  |  650行  |  21.91 KB

// 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