Commit 18029c02 authored by Clark DuVall's avatar Clark DuVall Committed by Commit Bot

[WebLayer] Allow associating embedder specific data with tabs

This adds a Tab.setData() and Tab.getData() API which allows setting
arbitrary embedder specific data on a tab. This data will be
saved/restored along with other browser data.

Bug: 1079313
Change-Id: Iccc0f17e647c4252768e2b1e69e2c87d9f695de1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2204540Reviewed-by: default avatarScott Violet <sky@chromium.org>
Commit-Queue: Clark DuVall <cduvall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#770274}
parent 202569f0
......@@ -68,6 +68,7 @@ static const SessionCommand::id_type kCommandSetTabGroupMetadata = 26;
static const SessionCommand::id_type kCommandSetTabGroupMetadata2 = 27;
static const SessionCommand::id_type kCommandSetTabGuid = 28;
static const SessionCommand::id_type kCommandSetTabUserAgentOverride2 = 29;
static const SessionCommand::id_type kCommandSetTabData = 30;
namespace {
......@@ -807,6 +808,31 @@ bool CreateTabsAndWindows(
break;
}
case kCommandSetTabData: {
std::unique_ptr<base::Pickle> pickle(command->PayloadAsPickle());
base::PickleIterator it(*pickle);
SessionID::id_type tab_id = -1;
int size = 0;
if (!it.ReadInt(&tab_id) || !it.ReadInt(&size)) {
DVLOG(1) << "Failed reading command " << command->id();
return true;
}
std::map<std::string, std::string> data;
for (int i = 0; i < size; i++) {
std::string key;
std::string value;
if (!it.ReadString(&key) || !it.ReadString(&value)) {
DVLOG(1) << "Failed reading command " << command->id();
return true;
}
data.insert({key, value});
}
GetTab(SessionID::FromSerializedValue(tab_id), tabs)->data =
std::move(data);
break;
}
default:
DVLOG(1) << "Failed reading an unknown command " << command->id();
return true;
......@@ -1022,6 +1048,19 @@ std::unique_ptr<SessionCommand> CreateSetTabGuidCommand(
return std::make_unique<SessionCommand>(kCommandSetTabGuid, pickle);
}
std::unique_ptr<SessionCommand> CreateSetTabDataCommand(
const SessionID& tab_id,
const std::map<std::string, std::string>& data) {
base::Pickle pickle;
pickle.WriteInt(tab_id.id());
pickle.WriteInt(data.size());
for (const auto& kv : data) {
pickle.WriteString(kv.first);
pickle.WriteString(kv.second);
}
return std::make_unique<SessionCommand>(kCommandSetTabData, pickle);
}
bool ReplacePendingCommand(CommandStorageManager* command_storage_manager,
std::unique_ptr<SessionCommand>* command) {
// We optimize page navigations, which can happen quite frequently and
......
......@@ -93,6 +93,10 @@ SESSIONS_EXPORT std::unique_ptr<SessionCommand> CreateSetTabGuidCommand(
const SessionID& tab_id,
const std::string& guid);
SESSIONS_EXPORT std::unique_ptr<SessionCommand> CreateSetTabDataCommand(
const SessionID& tab_id,
const std::map<std::string, std::string>& data);
// Searches for a pending command using |command_storage_manager| that can be
// replaced with |command|. If one is found, pending command is removed, the
// command is added to the pending commands (taken ownership) and true is
......
......@@ -101,6 +101,9 @@ struct SESSIONS_EXPORT SessionTab {
// guid associated with the tab, may be empty.
std::string guid;
// Data associated with the tab by the embedder.
std::map<std::string, std::string> data;
private:
DISALLOW_COPY_AND_ASSIGN(SessionTab);
};
......
......@@ -23,6 +23,9 @@ import org.chromium.weblayer.NavigationController;
import org.chromium.weblayer.Tab;
import org.chromium.weblayer.shell.InstrumentationActivity;
import java.util.HashMap;
import java.util.Map;
/**
* Tests that fragment lifecycle works as expected.
*/
......@@ -179,4 +182,44 @@ public class BrowserFragmentLifecycleTest {
() -> { return restoredTab.getGuid(); });
Assert.assertEquals(initialTabId, restoredTabId);
}
@Test
@SmallTest
@MinWebLayerVersion(85)
public void restoresTabData() throws Throwable {
Bundle extras = new Bundle();
extras.putString(InstrumentationActivity.EXTRA_PERSISTENCE_ID, "x");
Map<String, String> initialData = new HashMap<>();
initialData.put("foo", "bar");
restoresTabData(extras, initialData);
}
@Test
@SmallTest
@MinWebLayerVersion(85)
public void restoreTabDataAfterRecreate() throws Throwable {
Map<String, String> initialData = new HashMap<>();
initialData.put("foo", "bar");
restoresTabData(new Bundle(), initialData);
}
private void restoresTabData(Bundle extras, Map<String, String> initialData) {
String url = mActivityTestRule.getTestDataURL("simple_page.html");
mActivityTestRule.launchShellWithUrl(url, extras);
TestThreadUtils.runOnUiThreadBlocking(() -> {
Tab tab = mActivityTestRule.getActivity().getTab();
Assert.assertTrue(tab.getData().isEmpty());
tab.setData(initialData);
});
mActivityTestRule.recreateActivity();
Tab tab = getTab();
Assert.assertNotNull(tab);
waitForTabToFinishRestore(tab, url);
TestThreadUtils.runOnUiThreadBlocking(
() -> Assert.assertEquals(initialData, tab.getData()));
}
}
......@@ -18,6 +18,8 @@ import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.weblayer.Tab;
import org.chromium.weblayer.shell.InstrumentationActivity;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
/**
......@@ -171,4 +173,43 @@ public class TabTest {
});
callbackHelper.waitForFirst();
}
@Test
@SmallTest
@MinWebLayerVersion(85)
public void testSetData() {
String startupUrl = "about:blank";
mActivity = mActivityTestRule.launchShellWithUrl(startupUrl);
Map<String, String> data = new HashMap<>();
data.put("foo", "bar");
TestThreadUtils.runOnUiThreadBlocking(() -> {
Tab tab = mActivity.getTab();
tab.setData(data);
Assert.assertEquals(data.get("foo"), tab.getData().get("foo"));
tab.setData(new HashMap<>());
Assert.assertTrue(tab.getData().isEmpty());
});
}
@Test
@SmallTest
@MinWebLayerVersion(85)
public void testSetDataMaxSize() {
String startupUrl = "about:blank";
mActivity = mActivityTestRule.launchShellWithUrl(startupUrl);
Map<String, String> data = new HashMap<>();
data.put("big", new String(new char[10000]));
TestThreadUtils.runOnUiThreadBlocking(() -> {
try {
mActivity.getTab().setData(data);
} catch (IllegalArgumentException e) {
// Expected exception.
return;
}
Assert.fail("Expected IllegalArgumentException.");
});
}
}
......@@ -158,8 +158,7 @@ ScopedJavaLocalRef<jbyteArray> BrowserImpl::GetBrowserPersisterCryptoKey(
ScopedJavaLocalRef<jbyteArray> BrowserImpl::GetMinimalPersistenceState(
JNIEnv* env) {
auto state = GetMinimalPersistenceState();
return base::android::ToJavaByteArray(env, &(state.front()), state.size());
return base::android::ToJavaByteArray(env, GetMinimalPersistenceState());
}
void BrowserImpl::RestoreStateIfNecessary(
......
......@@ -534,6 +534,27 @@ public final class TabImpl extends ITab.Stub {
return TabImplJni.get().getGuid(mNativeTab);
}
@Override
public boolean setData(Map data) {
String[] flattenedMap = new String[data.size() * 2];
int i = 0;
for (Map.Entry<String, String> entry : ((Map<String, String>) data).entrySet()) {
flattenedMap[i++] = entry.getKey();
flattenedMap[i++] = entry.getValue();
}
return TabImplJni.get().setData(mNativeTab, flattenedMap);
}
@Override
public Map getData() {
String[] data = TabImplJni.get().getData(mNativeTab);
Map<String, String> map = new HashMap<>();
for (int i = 0; i < data.length; i += 2) {
map.put(data[i], data[i + 1]);
}
return map;
}
@Override
public void captureScreenShot(float scale, IObjectWrapper valueCallback) {
StrictModeWorkaround.apply();
......@@ -762,5 +783,7 @@ public final class TabImpl extends ITab.Stub {
String getGuid(long nativeTabImpl);
void captureScreenShot(long nativeTabImpl, float scale,
ValueCallback<Pair<Bitmap, Integer>> valueCallback);
boolean setData(long nativeTabImpl, String[] data);
String[] getData(long nativeTabImpl);
}
}
......@@ -51,4 +51,10 @@ interface ITab {
// Added in 84
void captureScreenShot(in float scale, in IObjectWrapper resultCallback) = 16;
// Added in 85
boolean setData(in Map data) = 17;
// Added in 85
Map getData() = 18;
}
......@@ -84,6 +84,7 @@ void ProcessRestoreCommands(
DCHECK(entries.empty());
TabImpl* tab = browser->CreateTabForSessionRestore(std::move(web_contents),
session_tab.guid);
tab->SetData(session_tab.data);
if (!had_tabs && i == (windows[0])->selected_tab_index)
browser->SetActiveTab(tab);
......@@ -142,6 +143,8 @@ BuildCommandsForTabConfiguration(const SessionID& browser_session_id,
result.push_back(sessions::CreateSetTabGuidCommand(tab_id, tab->GetGuid()));
result.push_back(sessions::CreateSetTabDataCommand(tab_id, tab->GetData()));
return result;
}
......
......@@ -105,8 +105,9 @@ void BrowserPersister::OnGeneratedNewCryptoKey(
}
void BrowserPersister::OnTabAdded(Tab* tab) {
content::WebContents* web_contents =
static_cast<TabImpl*>(tab)->web_contents();
auto* tab_impl = static_cast<TabImpl*>(tab);
data_observer_.Add(tab_impl);
content::WebContents* web_contents = tab_impl->web_contents();
auto* tab_helper = sessions::SessionTabHelper::FromWebContents(web_contents);
DCHECK(tab_helper);
tab_helper->SetWindowID(browser_session_id_);
......@@ -130,10 +131,11 @@ void BrowserPersister::OnTabAdded(Tab* tab) {
}
void BrowserPersister::OnTabRemoved(Tab* tab, bool active_tab_changed) {
auto* tab_impl = static_cast<TabImpl*>(tab);
data_observer_.Remove(tab_impl);
// Allow the associated sessionStorage to get deleted; it won't be needed
// in the session restore.
content::WebContents* web_contents =
static_cast<TabImpl*>(tab)->web_contents();
content::WebContents* web_contents = tab_impl->web_contents();
content::SessionStorageNamespace* session_storage_namespace =
web_contents->GetController().GetDefaultSessionStorageNamespace();
session_storage_namespace->SetShouldPersist(false);
......@@ -161,6 +163,16 @@ void BrowserPersister::OnActiveTabChanged(Tab* tab) {
browser_session_id_, index));
}
void BrowserPersister::OnDataChanged(
TabImpl* tab,
const std::map<std::string, std::string>& data) {
if (rebuild_on_next_save_)
return;
ScheduleCommand(
sessions::CreateSetTabDataCommand(GetSessionIDForTab(tab), data));
}
void BrowserPersister::SetTabUserAgentOverride(
const SessionID& window_id,
const SessionID& tab_id,
......
......@@ -14,10 +14,12 @@
#include <vector>
#include "base/macros.h"
#include "base/scoped_observer.h"
#include "base/task/cancelable_task_tracker.h"
#include "components/sessions/content/session_tab_helper_delegate.h"
#include "components/sessions/core/command_storage_manager_delegate.h"
#include "components/sessions/core/session_service_commands.h"
#include "weblayer/browser/tab_impl.h"
#include "weblayer/public/browser_observer.h"
class SessionID;
......@@ -29,7 +31,6 @@ class SessionCommand;
namespace weblayer {
class BrowserImpl;
class TabImpl;
// BrowserPersister is responsible for maintaining the state of tabs in a
// single Browser so that they can be restored at a later date. The state is
......@@ -40,7 +41,8 @@ class TabImpl;
// current state.
class BrowserPersister : public sessions::CommandStorageManagerDelegate,
public sessions::SessionTabHelperDelegate,
public BrowserObserver {
public BrowserObserver,
public TabImpl::DataObserver {
public:
BrowserPersister(const base::FilePath& path,
BrowserImpl* browser,
......@@ -72,6 +74,10 @@ class BrowserPersister : public sessions::CommandStorageManagerDelegate,
void OnTabRemoved(Tab* tab, bool active_tab_changed) override;
void OnActiveTabChanged(Tab* tab) override;
// TabImpl::DataObserver:
void OnDataChanged(TabImpl* tab,
const std::map<std::string, std::string>& data) override;
// sessions::SessionTabHelperDelegate:
void SetTabUserAgentOverride(const SessionID& window_id,
const SessionID& tab_id,
......@@ -127,6 +133,12 @@ class BrowserPersister : public sessions::CommandStorageManagerDelegate,
std::vector<uint8_t> crypto_key_;
ScopedObserver<TabImpl,
TabImpl::DataObserver,
&TabImpl::AddDataObserver,
&TabImpl::RemoveDataObserver>
data_observer_{this};
base::CancelableTaskTracker cancelable_task_tracker_;
};
......
......@@ -15,6 +15,7 @@
#include "content/public/test/url_loader_interceptor.h"
#include "net/base/filename_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "weblayer/browser/browser_impl.h"
#include "weblayer/browser/profile_impl.h"
#include "weblayer/browser/tab_impl.h"
......@@ -40,6 +41,7 @@ class BrowserPersisterTestHelper {
};
namespace {
using testing::UnorderedElementsAre;
class OneShotNavigationObserver : public NavigationObserver {
public:
......@@ -230,6 +232,63 @@ IN_PROC_BROWSER_TEST_F(BrowserPersisterTest, RestoresGuid) {
EXPECT_EQ(original_guid, browser->GetTabs()[0]->GetGuid());
}
IN_PROC_BROWSER_TEST_F(BrowserPersisterTest, RestoresData) {
ASSERT_TRUE(embedded_test_server()->Start());
std::unique_ptr<BrowserImpl> browser = CreateBrowser(GetProfile(), "x");
Tab* tab = browser->AddTab(Tab::Create(GetProfile()));
tab->SetData({{"abc", "efg"}});
const GURL url = embedded_test_server()->GetURL("/simple_page.html");
NavigateAndWaitForCompletion(url, tab);
ShutdownBrowserPersisterAndWait(browser.get());
tab = nullptr;
browser.reset();
browser = CreateBrowser(GetProfile(), "x");
// Should be no tabs while waiting for restore.
EXPECT_TRUE(browser->GetTabs().empty());
// Wait for the restore and navigation to complete.
BrowserNavigationObserverImpl::WaitForNewTabToCompleteNavigation(
browser.get(), url);
ASSERT_EQ(1u, browser->GetTabs().size());
EXPECT_EQ(browser->GetTabs()[0], browser->GetActiveTab());
EXPECT_THAT(browser->GetTabs()[0]->GetData(),
UnorderedElementsAre(std::make_pair("abc", "efg")));
}
IN_PROC_BROWSER_TEST_F(BrowserPersisterTest, RestoresMostRecentData) {
ASSERT_TRUE(embedded_test_server()->Start());
std::unique_ptr<BrowserImpl> browser = CreateBrowser(GetProfile(), "x");
Tab* tab = browser->AddTab(Tab::Create(GetProfile()));
tab->SetData({{"xxx", "xxx"}});
const GURL url = embedded_test_server()->GetURL("/simple_page.html");
NavigateAndWaitForCompletion(url, tab);
// Make sure the data has been saved, then set different data on the tab.
BrowserPersisterTestHelper::GetCommandStorageManager(
browser->browser_persister())
->Save();
tab->SetData({{"abc", "efg"}});
ShutdownBrowserPersisterAndWait(browser.get());
tab = nullptr;
browser.reset();
browser = CreateBrowser(GetProfile(), "x");
// Should be no tabs while waiting for restore.
EXPECT_TRUE(browser->GetTabs().empty());
// Wait for the restore and navigation to complete.
BrowserNavigationObserverImpl::WaitForNewTabToCompleteNavigation(
browser.get(), url);
ASSERT_EQ(1u, browser->GetTabs().size());
EXPECT_EQ(browser->GetTabs()[0], browser->GetActiveTab());
EXPECT_THAT(browser->GetTabs()[0]->GetData(),
UnorderedElementsAre(std::make_pair("abc", "efg")));
}
IN_PROC_BROWSER_TEST_F(BrowserPersisterTest, TwoTabs) {
ASSERT_TRUE(embedded_test_server()->Start());
......
......@@ -64,6 +64,7 @@
#if defined(OS_ANDROID)
#include "base/android/callback_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/json/json_writer.h"
#include "base/trace_event/trace_event.h"
......@@ -98,6 +99,9 @@ namespace weblayer {
namespace {
// Maximum size of data when calling SetData().
constexpr int kMaxDataSize = 4096;
#if defined(OS_ANDROID)
bool g_system_autofill_disabled_for_testing = false;
......@@ -329,6 +333,14 @@ TabImpl* TabImpl::FromWebContents(content::WebContents* web_contents) {
->controller;
}
void TabImpl::AddDataObserver(DataObserver* observer) {
data_observers_.AddObserver(observer);
}
void TabImpl::RemoveDataObserver(DataObserver* observer) {
data_observers_.RemoveObserver(observer);
}
void TabImpl::SetErrorPageDelegate(ErrorPageDelegate* delegate) {
error_page_delegate_ = delegate;
}
......@@ -387,6 +399,14 @@ const std::string& TabImpl::GetGuid() {
return guid_;
}
void TabImpl::SetData(const std::map<std::string, std::string>& data) {
DCHECK(SetDataInternal(data));
}
const std::map<std::string, std::string>& TabImpl::GetData() {
return data_;
}
void TabImpl::ExecuteScriptWithUserGestureForTests(
const base::string16& script) {
web_contents_->GetMainFrame()->ExecuteJavaScriptWithUserGestureForTests(
......@@ -596,6 +616,27 @@ void TabImpl::CaptureScreenShot(
base::BindOnce(&OnScreenShotCaptured,
ScopedJavaGlobalRef<jobject>(value_callback)));
}
jboolean TabImpl::SetData(
JNIEnv* env,
const base::android::JavaParamRef<jobjectArray>& data) {
std::vector<std::string> flattened_map;
base::android::AppendJavaStringArrayToStringVector(env, data, &flattened_map);
std::map<std::string, std::string> data_map;
for (size_t i = 0; i < flattened_map.size(); i += 2) {
data_map.insert({flattened_map[i], flattened_map[i + 1]});
}
return SetDataInternal(data_map);
}
base::android::ScopedJavaLocalRef<jobjectArray> TabImpl::GetData(JNIEnv* env) {
std::vector<std::string> flattened_map;
for (const auto& kv : data_) {
flattened_map.push_back(kv.first);
flattened_map.push_back(kv.second);
}
return base::android::ToJavaArrayOfStrings(env, flattened_map);
}
#endif // OS_ANDROID
content::WebContents* TabImpl::OpenURLFromTab(
......@@ -983,4 +1024,16 @@ sessions::SessionTabHelperDelegate* TabImpl::GetSessionServiceTabHelperDelegate(
return browser_ ? browser_->browser_persister() : nullptr;
}
bool TabImpl::SetDataInternal(const std::map<std::string, std::string>& data) {
int total_size = 0;
for (const auto& kv : data)
total_size += kv.first.size() + kv.second.size();
if (total_size > kMaxDataSize)
return false;
data_ = data;
for (auto& observer : data_observers_)
observer.OnDataChanged(this, data_);
return true;
}
} // namespace weblayer
......@@ -81,6 +81,14 @@ class TabImpl : public Tab,
kBitmapAllocationFailed,
};
class DataObserver {
public:
// Called when SetData() is called on |tab|.
virtual void OnDataChanged(
TabImpl* tab,
const std::map<std::string, std::string>& data) = 0;
};
// TODO(sky): investigate a better way to not have so many ifdefs.
#if defined(OS_ANDROID)
TabImpl(ProfileImpl* profile,
......@@ -154,10 +162,17 @@ class TabImpl : public Tab,
JNIEnv* env,
jfloat scale,
const base::android::JavaParamRef<jobject>& value_callback);
jboolean SetData(JNIEnv* env,
const base::android::JavaParamRef<jobjectArray>& data);
base::android::ScopedJavaLocalRef<jobjectArray> GetData(JNIEnv* env);
#endif
ErrorPageDelegate* error_page_delegate() { return error_page_delegate_; }
void AddDataObserver(DataObserver* observer);
void RemoveDataObserver(DataObserver* observer);
// Tab:
void SetErrorPageDelegate(ErrorPageDelegate* delegate) override;
void SetFullscreenDelegate(FullscreenDelegate* delegate) override;
......@@ -169,6 +184,8 @@ class TabImpl : public Tab,
bool use_separate_isolate,
JavaScriptResultCallback callback) override;
const std::string& GetGuid() override;
void SetData(const std::map<std::string, std::string>& data) override;
const std::map<std::string, std::string>& GetData() override;
#if !defined(OS_ANDROID)
void AttachToView(views::WebView* web_view) override;
#endif
......@@ -291,6 +308,8 @@ class TabImpl : public Tab,
void UpdateBrowserVisibleSecurityStateIfNecessary();
bool SetDataInternal(const std::map<std::string, std::string>& data);
BrowserImpl* browser_ = nullptr;
ErrorPageDelegate* error_page_delegate_ = nullptr;
FullscreenDelegate* fullscreen_delegate_ = nullptr;
......@@ -321,6 +340,9 @@ class TabImpl : public Tab,
const std::string guid_;
std::map<std::string, std::string> data_;
base::ObserverList<DataObserver>::Unchecked data_observers_;
base::string16 title_;
base::WeakPtrFactory<TabImpl> weak_ptr_factory_{this};
......
......@@ -344,6 +344,50 @@ public class Tab {
}
}
/**
* Set arbitrary data on the tab. This will be saved and restored with the browser, so it is
* important to keep this data as small as possible.
*
* @param data The data to set, must be smaller than 4K when serialized. A snapshot of this data
* is taken, so any changes to the passed in object after this call will not be reflected.
*
* @throws IllegalArgumentException if the serialzed size of the data exceeds 4K.
*
* @since 85
*/
public void setData(@NonNull Map<String, String> data) {
ThreadCheck.ensureOnUiThread();
if (WebLayer.getSupportedMajorVersionInternal() < 85) {
throw new UnsupportedOperationException();
}
try {
if (!mImpl.setData(data)) {
throw new IllegalArgumentException("Data given to Tab.setData() was too large.");
}
} catch (RemoteException e) {
throw new APICallException(e);
}
}
/**
* Get arbitrary data set on the tab with setData().
*
* @return the data or an empty map if no data was set.
* @since 85
*/
@NonNull
public Map<String, String> getData() {
ThreadCheck.ensureOnUiThread();
if (WebLayer.getSupportedMajorVersionInternal() < 85) {
throw new UnsupportedOperationException();
}
try {
return (Map<String, String>) mImpl.getData();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
private final class TabClientImpl extends ITabClient.Stub {
@Override
public void visibleUriChanged(String uriString) {
......
......@@ -75,6 +75,12 @@ class Tab {
// Returns the tab's guid.
virtual const std::string& GetGuid() = 0;
// Allows the embedder to get and set arbitrary data on the tab. This will be
// saved and restored with the browser, so it is important to keep this data
// as small as possible.
virtual void SetData(const std::map<std::string, std::string>& data) = 0;
virtual const std::map<std::string, std::string>& GetData() = 0;
#if !defined(OS_ANDROID)
// TODO: this isn't a stable API, so use it now for expediency in the C++ API,
// but if we ever want to have backward or forward compatibility in C++ this
......
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