Commit 0c471989 authored by Danyao Wang's avatar Danyao Wang Committed by Commit Bot

[Nav Experiment] Implement WKBasedNavigationManagerImpl::Restore().

restore_session.html is a magic file included in the app bundle that
helps WKBasedNavigationManager implement restore. The navigation manager
serializes and encodes the session history in the query parameter, and
loads restore_session.html to inject the history into the web view.

Bug: 734150
Change-Id: Ideaa7239eb3239322e3e72c5599736962962b465
Reviewed-on: https://chromium-review.googlesource.com/698465Reviewed-by: default avatarKurt Horimoto <kkhorimoto@chromium.org>
Commit-Queue: Kurt Horimoto <kkhorimoto@chromium.org>
Cr-Commit-Position: refs/heads/master@{#506847}
parent 204048a0
......@@ -25,6 +25,7 @@ source_set("web") {
deps = [
":core",
":js_resources",
":navigation_resources",
":reload_type",
":resources",
":user_agent",
......@@ -844,6 +845,15 @@ js_compile_checked("js_resources") {
]
}
bundle_data("navigation_resources") {
sources = [
"navigation/resources/restore_session.html",
]
outputs = [
"{{bundle_resources_dir}}/{{source_file_part}}",
]
}
grit("resources") {
source = "ios_web_resources.grd"
......
<!doctype html>
<html>
<head>
<script>
/**
* Main entry point. This page operates in two modes:
* 1. If ?session= is provided, restoreSession() parses URLs from the query
* parameter and uses History API to insert into the current session
* history an entry for each URL in order. Due to same origin policy,
* the entries link back to this page with the target URL encoded in
* the query parameter. The actual redirection takes place when user
* navigates to the restored session entries.
* 2. If ?targetUrl= is provided, redirect() immediately redirect the page
* to the URL encoded in the query parameter.
*/
window.onload = function() {
// Remember the base URL of this page before it is changed by the History
// API calls so it can be used to create redirect page URLs.
let baseUrl = [
window.location.protocol,
window.location.host,
window.location.pathname].join('');
if (window.location.search == '') {
handleError("Search query is mandatory");
}
let params = new URLSearchParams(document.location.search.substring(1));
let sessionHistory = params.get("session");
let targetUrl = params.get("targetUrl");
if (sessionHistory) {
restoreSession(baseUrl, sessionHistory);
} else if (targetUrl) {
redirect(targetUrl);
} else {
handleError("Missing both sessionHistory and targetUrl");
}
};
/**
* The window.onpopstate event fires when the user navigates to one of the
* history entries created by restoreSession(). This handler forces a reload
* to reidrect to the target URL.
*/
window.onpopstate = function(event) {
location.reload();
}
/**
* Manipulates the current session history to mimic the provided serialized
* history.
* @param {string} encodedSessionHistory An escaped string serialization of
* a JSON object that represents the session history to recreate. It
* contains three fields:
* urls: A list of strings that represent the URLs visited in the session
* in order.
* titles: A list of strings that represent the page title of each URL in
* the URL list. It must have the same length as 'urls'.
* offset: An non-positive integer that represents the last visible entry
* relative to the end of the list. This is used to jump back to the
* last visible entry after restoring the entire list.
* @param {string} baseUrl The URL of this page without the query parameter.
* The restored history entry initially points to this page with the
* target URL encoded in the query parameter. A user is redirected to the
* target URL when they navigates to the restored entry.
*/
function restoreSession(baseUrl, encodedSessionHistory) {
function getRestoreURL(targetUrl) {
return baseUrl + '?targetUrl=' + encodeURIComponent(targetUrl);
}
var sessionHistory = [];
try {
sessionHistory = JSON.parse(decodeURIComponent(encodedSessionHistory));
if (sessionHistory.urls.length < 1) {
handleError("sessionHistory is empty");
return;
}
history.replaceState(
null, /* state */
sessionHistory.titles[0] || "Untitled",
getRestoreURL(sessionHistory.urls[0] || "about:blank"));
for (var i = 1; i < sessionHistory.urls.length; i++) {
history.pushState(
null, /* state */
sessionHistory.titles[i] || "Untitled",
getRestoreURL(sessionHistory.urls[i] || "about:blank"));
}
var currentItemOffset = parseInt(sessionHistory.offset);
history.go(currentItemOffset);
} catch (e) {
handleError(e.name + ": " + e.message);
}
}
/**
* Redirects to a target URL.
* @param {string} encodedTargetUrl An escaped version of the target URL.
*/
function redirect(encodedTargetUrl) {
var target = decodeURIComponent(encodedTargetUrl);
window.location.replace(target);
}
/**
* Sends error message to native controller.
*/
function handleError(message) {
console.log("Error: " + message);
if (__gCrWeb.message) {
__gCrWeb.message.invokeOnHost(
{"command": "restoresession.error", "message": message});
__gCrWeb.message.invokeQueues();
}
}
</script>
</head>
<body>
</body>
</html>
......@@ -7,16 +7,23 @@
#import <Foundation/Foundation.h>
#include <memory>
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/mac/bundle_locations.h"
#include "base/memory/ptr_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/values.h"
#import "ios/web/navigation/crw_navigation_item_holder.h"
#import "ios/web/navigation/navigation_item_impl.h"
#include "ios/web/navigation/navigation_item_impl_list.h"
#import "ios/web/navigation/navigation_manager_delegate.h"
#include "ios/web/public/load_committed_details.h"
#import "ios/web/public/navigation_item.h"
#import "ios/web/public/web_client.h"
#import "ios/web/web_state/ui/crw_web_view_navigation_proxy.h"
#import "net/base/mac/url_conversions.h"
#include "net/base/url_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
......@@ -346,7 +353,52 @@ bool WKBasedNavigationManagerImpl::CanPruneAllButLastCommittedItem() const {
void WKBasedNavigationManagerImpl::Restore(
int last_committed_item_index,
std::vector<std::unique_ptr<NavigationItem>> items) {
DLOG(WARNING) << "Not yet implemented.";
DCHECK(GetItemCount() == 0 && !GetPendingItem());
DCHECK_LT(last_committed_item_index, static_cast<int>(items.size()));
DCHECK(items.empty() || last_committed_item_index >= 0);
if (items.empty())
return;
// This function restores session history by loading a magic local file
// (restore_session.html) into the web view. The session history is encoded
// in the query parameter. When loaded, restore_session.html parses the
// session history and replays them into the web view using History API.
// TODO(crbug.com/771200): Retain these original NavigationItems restored from
// storage and associate them with new WKBackForwardListItems created after
// history restore so information such as scroll position is restored.
// The URLs and titles of the restored entries are stored in two separate
// lists instead of a single list of objects to reduce the size of the JSON
// string to be included in the query parameter.
base::Value restored_urls(base::Value::Type::LIST);
base::Value restored_titles(base::Value::Type::LIST);
restored_urls.GetList().reserve(items.size());
restored_titles.GetList().reserve(items.size());
for (size_t index = 0; index < items.size(); index++) {
restored_urls.GetList().push_back(
base::Value(items[index]->GetURL().spec()));
restored_titles.GetList().push_back(base::Value(items[index]->GetTitle()));
}
base::Value session(base::Value::Type::DICTIONARY);
int offset = last_committed_item_index + 1 - items.size();
session.SetKey("offset", base::Value(offset));
session.SetKey("urls", std::move(restored_urls));
session.SetKey("titles", std::move(restored_titles));
std::string session_json;
base::JSONWriter::Write(session, &session_json);
std::string restore_session_resource_path = base::SysNSStringToUTF8(
[base::mac::FrameworkBundle() pathForResource:@"restore_session"
ofType:@"html"]);
std::string url_spec = base::StringPrintf(
"%s://%s", url::kFileScheme, restore_session_resource_path.c_str());
GURL url = net::AppendQueryParameter(GURL(url_spec), "session", session_json);
WebLoadParams params(url);
params.transition_type = ui::PAGE_TRANSITION_RELOAD;
LoadURLWithParams(params);
}
NavigationItemImpl* WKBasedNavigationManagerImpl::GetNavigationItemImplAtIndex(
......
......@@ -7,14 +7,19 @@
#include <WebKit/WebKit.h>
#include <memory>
#include "base/memory/ptr_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "ios/web/navigation/navigation_manager_delegate.h"
#import "ios/web/navigation/navigation_manager_impl.h"
#include "ios/web/public/load_committed_details.h"
#include "ios/web/public/navigation_item.h"
#include "ios/web/public/test/fakes/test_browser_state.h"
#import "ios/web/public/web_client.h"
#import "ios/web/test/fakes/crw_test_back_forward_list.h"
#include "ios/web/test/fakes/test_navigation_manager_delegate.h"
#include "ios/web/test/test_url_constants.h"
#include "net/base/url_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/platform_test.h"
#include "third_party/ocmock/OCMock/OCMock.h"
......@@ -53,6 +58,32 @@ bool WebUIUrlRewriter(GURL* url, BrowserState* browser_state) {
return false;
}
class MockNavigationManagerDelegate : public NavigationManagerDelegate {
public:
void SetWebViewNavigationProxy(id web_view) { mock_web_view_ = web_view; }
MOCK_METHOD0(ClearTransientContent, void());
MOCK_METHOD0(RecordPageStateInNavigationItem, void());
MOCK_METHOD0(UpdateHtml5HistoryState, void());
MOCK_METHOD1(WillLoadCurrentItemWithUrl, void(const GURL&));
MOCK_METHOD0(WillChangeUserAgentType, void());
MOCK_METHOD0(LoadCurrentItem, void());
MOCK_METHOD0(LoadIfNecessary, void());
MOCK_METHOD0(Reload, void());
MOCK_METHOD1(OnNavigationItemsPruned, void(size_t));
MOCK_METHOD0(OnNavigationItemChanged, void());
MOCK_METHOD1(OnNavigationItemCommitted, void(const LoadCommittedDetails&));
private:
WebState* GetWebState() override { return nullptr; }
id<CRWWebViewNavigationProxy> GetWebViewNavigationProxy() const override {
return mock_web_view_;
}
id mock_web_view_;
};
// Test fixture for WKBasedNavigationManagerImpl.
class WKBasedNavigationManagerTest : public PlatformTest {
protected:
......@@ -69,14 +100,13 @@ class WKBasedNavigationManagerTest : public PlatformTest {
url::AddStandardScheme(kSchemeToRewrite, url::SCHEME_WITHOUT_PORT);
}
protected:
std::unique_ptr<NavigationManagerImpl> manager_;
CRWTestBackForwardList* mock_wk_list_;
id mock_web_view_;
MockNavigationManagerDelegate delegate_;
private:
TestBrowserState browser_state_;
TestNavigationManagerDelegate delegate_;
};
// Tests that GetItemAtIndex() on an empty manager will sync navigation items to
......@@ -531,4 +561,42 @@ TEST_F(WKBasedNavigationManagerTest, CanGoToOffset) {
EXPECT_FALSE(manager_->CanGoToOffset(2));
}
// Tests that non-empty session history can be restored.
TEST_F(WKBasedNavigationManagerTest, RestoreSessionWithHistory) {
auto item0 = base::MakeUnique<NavigationItemImpl>();
item0->SetURL(GURL("http://www.0.com"));
item0->SetTitle(base::ASCIIToUTF16("Test Website 0"));
auto item1 = base::MakeUnique<NavigationItemImpl>();
item1->SetURL(GURL("http://www.1.com"));
std::vector<std::unique_ptr<NavigationItem>> items;
items.push_back(std::move(item0));
items.push_back(std::move(item1));
EXPECT_CALL(delegate_, WillLoadCurrentItemWithUrl(testing::_)).Times(1);
manager_->Restore(0 /* last_committed_item_index */, std::move(items));
NavigationItem* pending_item = manager_->GetPendingItem();
ASSERT_TRUE(pending_item != nullptr);
GURL pending_url = pending_item->GetURL();
EXPECT_TRUE(pending_url.SchemeIsFile());
EXPECT_EQ("restore_session.html", pending_url.ExtractFileName());
std::string session_json;
net::GetValueForKeyInQuery(pending_url, "session", &session_json);
EXPECT_EQ(
"{\"offset\":-1,\"titles\":[\"Test Website 0\",\"\"],"
"\"urls\":[\"http://www.0.com/\",\"http://www.1.com/\"]}",
session_json);
}
// Tests that Restore() accepts empty session history and performs no-op.
TEST_F(WKBasedNavigationManagerTest, RestoreSessionWithEmptyHistory) {
EXPECT_CALL(delegate_, WillLoadCurrentItemWithUrl(testing::_)).Times(0);
manager_->Restore(-1 /* last_committed_item_index */,
std::vector<std::unique_ptr<NavigationItem>>());
ASSERT_EQ(nullptr, manager_->GetPendingItem());
}
} // namespace web
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment