Commit e21f723d authored by Collin Baker's avatar Collin Baker Committed by Commit Bot

Send tab thumbnails to chrome://tab-strip

This connects the WebUI at chrome://tab-strip to the ThumbnailImage
infrastructure through an intermediate class ThumbnailTracker.

Bug: 991393
Change-Id: I7a730dea9f90db420c32b7cc4871b0608f447c2d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1758920
Commit-Queue: Collin Baker <collinbaker@chromium.org>
Reviewed-by: default avatarAlbert J. Wong <ajwong@chromium.org>
Reviewed-by: default avatarPeter Boström <pbos@chromium.org>
Reviewed-by: default avatarJohn Lee <johntlee@chromium.org>
Reviewed-by: default avatarRebekah Potter <rbpotter@chromium.org>
Reviewed-by: default avatarDana Fried <dfried@chromium.org>
Cr-Commit-Position: refs/heads/master@{#693911}
parent e3399315
...@@ -10,15 +10,20 @@ ...@@ -10,15 +10,20 @@
namespace base { namespace base {
void Base64Encode(const StringPiece& input, std::string* output) { std::string Base64Encode(span<const uint8_t> input) {
std::string temp; std::string output;
temp.resize(modp_b64_encode_len(input.size())); // makes room for null byte output.resize(modp_b64_encode_len(input.size())); // makes room for null byte
// modp_b64_encode_len() returns at least 1, so temp[0] is safe to use. // modp_b64_encode_len() returns at least 1, so output[0] is safe to use.
size_t output_size = modp_b64_encode(&(temp[0]), input.data(), input.size()); const size_t output_size = modp_b64_encode(
&(output[0]), reinterpret_cast<const char*>(input.data()), input.size());
temp.resize(output_size); // strips off null byte output.resize(output_size);
output->swap(temp); return output;
}
void Base64Encode(const StringPiece& input, std::string* output) {
*output = Base64Encode(base::as_bytes(base::make_span(input)));
} }
bool Base64Decode(const StringPiece& input, std::string* output) { bool Base64Decode(const StringPiece& input, std::string* output) {
......
...@@ -8,11 +8,15 @@ ...@@ -8,11 +8,15 @@
#include <string> #include <string>
#include "base/base_export.h" #include "base/base_export.h"
#include "base/containers/span.h"
#include "base/strings/string_piece.h" #include "base/strings/string_piece.h"
namespace base { namespace base {
// Encodes the input string in base64. The encoding can be done in-place. // Encodes the input binary data in base64.
BASE_EXPORT std::string Base64Encode(span<const uint8_t> input);
// Encodes the input string in base64.
BASE_EXPORT void Base64Encode(const StringPiece& input, std::string* output); BASE_EXPORT void Base64Encode(const StringPiece& input, std::string* output);
// Decodes the base64 input string. Returns true if successful and false // Decodes the base64 input string. Returns true if successful and false
......
...@@ -10,11 +10,18 @@ ...@@ -10,11 +10,18 @@
// Encode some random data, and then decode it. // Encode some random data, and then decode it.
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
std::string encode_output; base::span<const uint8_t> data_span(data, size);
std::string decode_output;
base::StringPiece data_piece(reinterpret_cast<const char*>(data), size); base::StringPiece data_piece(reinterpret_cast<const char*>(data), size);
base::Base64Encode(data_piece, &encode_output);
const std::string encode_output = base::Base64Encode(data_span);
std::string decode_output;
CHECK(base::Base64Decode(encode_output, &decode_output)); CHECK(base::Base64Decode(encode_output, &decode_output));
CHECK_EQ(data_piece, decode_output); CHECK_EQ(data_piece, decode_output);
// Also run the StringPiece variant and check that it gives the same results.
std::string string_piece_encode_output;
base::Base64Encode(data_piece, &string_piece_encode_output);
CHECK_EQ(encode_output, string_piece_encode_output);
return 0; return 0;
} }
...@@ -24,6 +24,20 @@ TEST(Base64Test, Basic) { ...@@ -24,6 +24,20 @@ TEST(Base64Test, Basic) {
EXPECT_EQ(kText, decoded); EXPECT_EQ(kText, decoded);
} }
TEST(Base64Test, Binary) {
const uint8_t kData[] = {0x00, 0x01, 0xFE, 0xFF};
std::string binary_encoded = Base64Encode(make_span(kData));
// Check that encoding the same data through the StringPiece interface gives
// the same results.
std::string string_piece_encoded;
Base64Encode(StringPiece(reinterpret_cast<const char*>(kData), sizeof(kData)),
&string_piece_encoded);
EXPECT_EQ(binary_encoded, string_piece_encoded);
}
TEST(Base64Test, InPlace) { TEST(Base64Test, InPlace) {
const std::string kText = "hello world"; const std::string kText = "hello world";
const std::string kBase64Text = "aGVsbG8gd29ybGQ="; const std::string kBase64Text = "aGVsbG8gd29ybGQ=";
......
...@@ -72,6 +72,12 @@ ...@@ -72,6 +72,12 @@
flex: 1; flex: 1;
} }
#thumbnailImg {
height: 100%;
object-fit: contain;
width: 100%;
}
/* Pinned tab styles */ /* Pinned tab styles */
:host([pinned]) { :host([pinned]) {
height: 50px; height: 50px;
...@@ -98,4 +104,6 @@ ...@@ -98,4 +104,6 @@
</button> </button>
</header> </header>
<div id="thumbnail"></div> <div id="thumbnail">
<img id="thumbnailImg">
</div>
...@@ -25,6 +25,15 @@ export class TabElement extends CustomElement { ...@@ -25,6 +25,15 @@ export class TabElement extends CustomElement {
this.faviconEl_ = this.faviconEl_ =
/** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#favicon')); /** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#favicon'));
/** @private {!HTMLElement} */
this.thumbnailContainer_ =
/** @type {!HTMLElement} */ (
this.shadowRoot.querySelector('#thumbnail'));
/** @private {!Image} */
this.thumbnail_ =
/** @type {!Image} */ (this.shadowRoot.querySelector('#thumbnailImg'));
/** @private {!Tab} */ /** @private {!Tab} */
this.tab_; this.tab_;
...@@ -64,9 +73,21 @@ export class TabElement extends CustomElement { ...@@ -64,9 +73,21 @@ export class TabElement extends CustomElement {
// Expose the ID to an attribute to allow easy querySelector use // Expose the ID to an attribute to allow easy querySelector use
this.setAttribute('data-tab-id', tab.id); this.setAttribute('data-tab-id', tab.id);
if (!this.tab_ || this.tab_.id !== tab.id) {
// Request thumbnail updates
chrome.send('addTrackedTab', [tab.id]);
}
this.tab_ = Object.freeze(tab); this.tab_ = Object.freeze(tab);
} }
/**
* @param {string} imgData
*/
updateThumbnail(imgData) {
this.thumbnail_.src = imgData;
}
/** @private */ /** @private */
onClick_() { onClick_() {
if (!this.tab_) { if (!this.tab_) {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import './tab.js'; import './tab.js';
import {addWebUIListener} from 'chrome://resources/js/cr.m.js';
import {CustomElement} from './custom_element.js'; import {CustomElement} from './custom_element.js';
import {TabElement} from './tab.js'; import {TabElement} from './tab.js';
import {TabsApiProxy} from './tabs_api_proxy.js'; import {TabsApiProxy} from './tabs_api_proxy.js';
...@@ -45,6 +46,9 @@ class TabListElement extends CustomElement { ...@@ -45,6 +46,9 @@ class TabListElement extends CustomElement {
/** @private {number} */ /** @private {number} */
this.windowId_; this.windowId_;
addWebUIListener(
'tab-thumbnail-updated', this.tabThumbnailUpdated_.bind(this));
} }
/** /**
...@@ -221,6 +225,18 @@ class TabListElement extends CustomElement { ...@@ -221,6 +225,18 @@ class TabListElement extends CustomElement {
} }
} }
/**
* @param {number} tabId
* @param {string} imgData
* @private
*/
tabThumbnailUpdated_(tabId, imgData) {
const tab = this.findTabElement_(tabId);
if (tab) {
tab.updateThumbnail(imgData);
}
}
/** @private */ /** @private */
updatePinnedTabsState_() { updatePinnedTabsState_() {
this.pinnedTabsContainerElement_.toggleAttribute( this.pinnedTabsContainerElement_.toggleAttribute(
......
...@@ -3974,6 +3974,8 @@ jumbo_split_static_library("ui") { ...@@ -3974,6 +3974,8 @@ jumbo_split_static_library("ui") {
"views/frame/webui_tab_strip_container_view.h", "views/frame/webui_tab_strip_container_view.h",
"webui/tab_strip/tab_strip_ui.cc", "webui/tab_strip/tab_strip_ui.cc",
"webui/tab_strip/tab_strip_ui.h", "webui/tab_strip/tab_strip_ui.h",
"webui/tab_strip/thumbnail_tracker.cc",
"webui/tab_strip/thumbnail_tracker.h",
] ]
} }
} }
......
...@@ -306,7 +306,8 @@ void TabHelpers::AttachTabHelpers(WebContents* web_contents) { ...@@ -306,7 +306,8 @@ void TabHelpers::AttachTabHelpers(WebContents* web_contents) {
safe_browsing::SafeBrowsingTabObserver::CreateForWebContents(web_contents); safe_browsing::SafeBrowsingTabObserver::CreateForWebContents(web_contents);
SearchTabHelper::CreateForWebContents(web_contents); SearchTabHelper::CreateForWebContents(web_contents);
TabDialogs::CreateForWebContents(web_contents); TabDialogs::CreateForWebContents(web_contents);
if (base::FeatureList::IsEnabled(features::kTabHoverCardImages)) if (base::FeatureList::IsEnabled(features::kTabHoverCardImages) ||
base::FeatureList::IsEnabled(features::kWebUITabStrip))
ThumbnailTabHelper::CreateForWebContents(web_contents); ThumbnailTabHelper::CreateForWebContents(web_contents);
web_modal::WebContentsModalDialogManager::CreateForWebContents(web_contents); web_modal::WebContentsModalDialogManager::CreateForWebContents(web_contents);
#endif #endif
......
...@@ -96,8 +96,11 @@ void ThumbnailImage::AssignJPEGData(std::vector<uint8_t> data) { ...@@ -96,8 +96,11 @@ void ThumbnailImage::AssignJPEGData(std::vector<uint8_t> data) {
} }
bool ThumbnailImage::ConvertJPEGDataToImageSkiaAndNotifyObservers() { bool ThumbnailImage::ConvertJPEGDataToImageSkiaAndNotifyObservers() {
if (!data_) if (!data_) {
if (async_operation_finished_callback_)
async_operation_finished_callback_.Run();
return false; return false;
}
return base::PostTaskAndReplyWithResult( return base::PostTaskAndReplyWithResult(
FROM_HERE, FROM_HERE,
{base::ThreadPool(), base::TaskPriority::USER_VISIBLE, {base::ThreadPool(), base::TaskPriority::USER_VISIBLE,
...@@ -109,6 +112,8 @@ bool ThumbnailImage::ConvertJPEGDataToImageSkiaAndNotifyObservers() { ...@@ -109,6 +112,8 @@ bool ThumbnailImage::ConvertJPEGDataToImageSkiaAndNotifyObservers() {
void ThumbnailImage::NotifyObservers(gfx::ImageSkia image) { void ThumbnailImage::NotifyObservers(gfx::ImageSkia image) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (async_operation_finished_callback_)
async_operation_finished_callback_.Run();
for (auto& observer : observers_) for (auto& observer : observers_)
observer.OnThumbnailImageAvailable(image); observer.OnThumbnailImageAvailable(image);
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#ifndef CHROME_BROWSER_UI_THUMBNAILS_THUMBNAIL_IMAGE_H_ #ifndef CHROME_BROWSER_UI_THUMBNAILS_THUMBNAIL_IMAGE_H_
#define CHROME_BROWSER_UI_THUMBNAILS_THUMBNAIL_IMAGE_H_ #define CHROME_BROWSER_UI_THUMBNAILS_THUMBNAIL_IMAGE_H_
#include <utility>
#include <vector> #include <vector>
#include "base/callback.h" #include "base/callback.h"
...@@ -55,6 +56,11 @@ class ThumbnailImage : public base::RefCounted<ThumbnailImage> { ...@@ -55,6 +56,11 @@ class ThumbnailImage : public base::RefCounted<ThumbnailImage> {
// long it will take, though in most cases it should happen very quickly. // long it will take, though in most cases it should happen very quickly.
virtual void RequestThumbnailImage(); virtual void RequestThumbnailImage();
void set_async_operation_finished_callback_for_testing(
base::RepeatingClosure callback) {
async_operation_finished_callback_ = std::move(callback);
}
private: private:
friend class Delegate; friend class Delegate;
friend class base::RefCounted<ThumbnailImage>; friend class base::RefCounted<ThumbnailImage>;
...@@ -71,6 +77,12 @@ class ThumbnailImage : public base::RefCounted<ThumbnailImage> { ...@@ -71,6 +77,12 @@ class ThumbnailImage : public base::RefCounted<ThumbnailImage> {
base::ObserverList<Observer> observers_; base::ObserverList<Observer> observers_;
// Called when an asynchronous operation (such as encoding image data upon
// assignment or decoding image data for observers) finishes or fails.
// Intended for unit tests that want to wait for internal operations following
// AssignSkBitmap() or RequestThumbnailImage() calls.
base::RepeatingClosure async_operation_finished_callback_;
SEQUENCE_CHECKER(sequence_checker_); SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<ThumbnailImage> weak_ptr_factory_{this}; base::WeakPtrFactory<ThumbnailImage> weak_ptr_factory_{this};
......
...@@ -69,11 +69,10 @@ const base::Feature kTabOutlinesInLowContrastThemes{ ...@@ -69,11 +69,10 @@ const base::Feature kTabOutlinesInLowContrastThemes{
const base::Feature kWebFooterExperiment{"WebFooterExperiment", const base::Feature kWebFooterExperiment{"WebFooterExperiment",
base::FEATURE_DISABLED_BY_DEFAULT}; base::FEATURE_DISABLED_BY_DEFAULT};
#if BUILDFLAG(ENABLE_WEBUI_TAB_STRIP) // Enables a web-based toolbar. See https://crbug.com/989131. Note this feature
// Enables a web-based toolbar. See https://crbug.com/989131 // only works when the ENABLE_WEBUI_TAB_STRIP buildflag is enabled.
const base::Feature kWebUITabStrip{"WebUITabStrip", const base::Feature kWebUITabStrip{"WebUITabStrip",
base::FEATURE_DISABLED_BY_DEFAULT}; base::FEATURE_DISABLED_BY_DEFAULT};
#endif // BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
#if defined(OS_LINUX) && !defined(OS_CHROMEOS) #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
constexpr base::Feature kEnableDbusAndX11StatusIcons{ constexpr base::Feature kEnableDbusAndX11StatusIcons{
......
...@@ -44,9 +44,7 @@ extern const base::Feature kTabOutlinesInLowContrastThemes; ...@@ -44,9 +44,7 @@ extern const base::Feature kTabOutlinesInLowContrastThemes;
extern const base::Feature kWebFooterExperiment; extern const base::Feature kWebFooterExperiment;
#if BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
extern const base::Feature kWebUITabStrip; extern const base::Feature kWebUITabStrip;
#endif // BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
#if defined(OS_LINUX) && !defined(OS_CHROMEOS) #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
extern const base::Feature kEnableDbusAndX11StatusIcons; extern const base::Feature kEnableDbusAndX11StatusIcons;
......
...@@ -4,6 +4,14 @@ ...@@ -4,6 +4,14 @@
#include "chrome/browser/ui/webui/tab_strip/tab_strip_ui.h" #include "chrome/browser/ui/webui/tab_strip/tab_strip_ui.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/favicon_source.h" #include "chrome/browser/ui/webui/favicon_source.h"
#include "chrome/common/webui_url_constants.h" #include "chrome/common/webui_url_constants.h"
...@@ -12,6 +20,120 @@ ...@@ -12,6 +20,120 @@
#include "components/favicon_base/favicon_url_parser.h" #include "components/favicon_base/favicon_url_parser.h"
#include "content/public/browser/url_data_source.h" #include "content/public/browser/url_data_source.h"
#include "content/public/browser/web_ui_data_source.h" #include "content/public/browser/web_ui_data_source.h"
#include "content/public/browser/web_ui_message_handler.h"
#include "third_party/skia/include/core/SkImageEncoder.h"
#include "third_party/skia/include/core/SkStream.h"
namespace {
// Writes bytes to a std::vector that can be fetched. This is used to record the
// output of skia image encoding.
//
// TODO(crbug.com/991393): remove this when we no longer transcode images here.
class BufferWStream : public SkWStream {
public:
BufferWStream() = default;
~BufferWStream() override = default;
// Returns the output buffer by moving.
std::vector<unsigned char> GetBuffer() { return std::move(result_); }
// SkWStream:
bool write(const void* buffer, size_t size) override {
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(buffer);
result_.insert(result_.end(), bytes, bytes + size);
return true;
}
size_t bytesWritten() const override { return result_.size(); }
private:
std::vector<unsigned char> result_;
};
class TabStripUIHandler : public content::WebUIMessageHandler {
public:
explicit TabStripUIHandler(Profile* profile)
: profile_(profile),
thumbnail_tracker_(base::Bind(&TabStripUIHandler::HandleThumbnailUpdate,
base::Unretained(this))) {}
~TabStripUIHandler() override = default;
protected:
void RegisterMessages() override {
web_ui()->RegisterMessageCallback(
"addTrackedTab",
base::Bind(&TabStripUIHandler::AddTrackedTab, base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"removeTrackedTab", base::Bind(&TabStripUIHandler::RemoveTrackedTab,
base::Unretained(this)));
}
private:
void AddTrackedTab(const base::ListValue* args) {
AllowJavascript();
int tab_id = 0;
if (!args->GetInteger(0, &tab_id))
return;
content::WebContents* tab = nullptr;
if (!extensions::ExtensionTabUtil::GetTabById(tab_id, profile_, true,
&tab)) {
// ID didn't refer to a valid tab.
DVLOG(1) << "Invalid tab ID";
return;
}
thumbnail_tracker_.WatchTab(tab);
}
void RemoveTrackedTab(const base::ListValue* args) {
AllowJavascript();
int tab_id = 0;
if (!args->GetInteger(0, &tab_id))
return;
// TODO(crbug.com/991393): un-watch tabs when we are done.
}
// Callback passed to |thumbnail_tracker_|. Called when a tab's thumbnail
// changes, or when we start watching the tab.
void HandleThumbnailUpdate(content::WebContents* tab, gfx::ImageSkia image) {
const SkBitmap& bitmap =
image.GetRepresentation(web_ui()->GetDeviceScaleFactor()).GetBitmap();
BufferWStream stream;
const bool encoding_succeeded =
SkEncodeImage(&stream, bitmap, SkEncodedImageFormat::kJPEG, 100);
DCHECK(encoding_succeeded);
const std::vector<unsigned char> image_data = stream.GetBuffer();
// Send base-64 encoded image to JS side.
//
// TODO(crbug.com/991393): streamline the process from tab capture to
// sending the image. Currently, a frame is captured, sent to
// ThumbnailImage, encoded to JPEG (w/ a copy), decoded to a raw bitmap
// (another copy), and sent to observers. Here, we then re-encode the image
// to a JPEG (another copy), encode to base64 (another copy), append the
// base64 header (another copy), and send it (another copy). This is 6
// copies of essentially the same image, and it is de-encoded and re-encoded
// to the same format. We can reduce the number of copies and avoid the
// redundant encoding.
std::string encoded_image =
base::Base64Encode(base::as_bytes(base::make_span(image_data)));
encoded_image = "data:image/jpeg;base64," + encoded_image;
const int tab_id = extensions::ExtensionTabUtil::GetTabId(tab);
FireWebUIListener("tab-thumbnail-updated", base::Value(tab_id),
base::Value(encoded_image));
}
Profile* profile_;
ThumbnailTracker thumbnail_tracker_;
DISALLOW_COPY_AND_ASSIGN(TabStripUIHandler);
};
} // namespace
TabStripUI::TabStripUI(content::WebUI* web_ui) TabStripUI::TabStripUI(content::WebUI* web_ui)
: content::WebUIController(web_ui) { : content::WebUIController(web_ui) {
...@@ -37,6 +159,8 @@ TabStripUI::TabStripUI(content::WebUI* web_ui) ...@@ -37,6 +159,8 @@ TabStripUI::TabStripUI(content::WebUI* web_ui)
content::URLDataSource::Add( content::URLDataSource::Add(
profile, std::make_unique<FaviconSource>( profile, std::make_unique<FaviconSource>(
profile, chrome::FaviconUrlFormat::kFavicon2)); profile, chrome::FaviconUrlFormat::kFavicon2));
web_ui->AddMessageHandler(std::make_unique<TabStripUIHandler>(profile));
} }
TabStripUI::~TabStripUI() {} TabStripUI::~TabStripUI() {}
...@@ -6,16 +6,19 @@ ...@@ -6,16 +6,19 @@
#define CHROME_BROWSER_UI_WEBUI_TAB_STRIP_TAB_STRIP_UI_H_ #define CHROME_BROWSER_UI_WEBUI_TAB_STRIP_TAB_STRIP_UI_H_
#include "base/macros.h" #include "base/macros.h"
#include "chrome/browser/ui/webui/tab_strip/thumbnail_tracker.h"
#include "content/public/browser/web_ui_controller.h" #include "content/public/browser/web_ui_controller.h"
// The WebUI version of the tab strip in the browser. It is currently only // The WebUI version of the tab strip in the browser. It is currently only
// supported on ChromeOS in tablet mode. // supported on ChromeOS in tablet mode.
class TabStripUI : public content::WebUIController { class TabStripUI : public content::WebUIController {
public: public:
TabStripUI(content::WebUI* web_ui); explicit TabStripUI(content::WebUI* web_ui);
~TabStripUI() override; ~TabStripUI() override;
private: private:
void HandleThumbnailUpdate(int extension_tab_id, gfx::ImageSkia image);
DISALLOW_COPY_AND_ASSIGN(TabStripUI); DISALLOW_COPY_AND_ASSIGN(TabStripUI);
}; };
......
// Copyright 2019 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 "chrome/browser/ui/webui/tab_strip/thumbnail_tracker.h"
#include <memory>
#include <utility>
#include "base/scoped_observer.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/thumbnails/thumbnail_image.h"
#include "chrome/browser/ui/thumbnails/thumbnail_tab_helper.h"
#include "content/public/browser/web_contents_observer.h"
// Handles requests for a given tab's thumbnail and watches for thumbnail
// updates for the lifetime of the tab.
class ThumbnailTracker::ContentsData : public content::WebContentsObserver,
public ThumbnailImage::Observer {
public:
ContentsData(ThumbnailTracker* parent, content::WebContents* contents)
: content::WebContentsObserver(contents),
parent_(parent),
observer_(this) {
thumbnail_ = parent_->thumbnail_getter_.Run(contents);
if (thumbnail_)
observer_.Add(thumbnail_.get());
}
void RequestThumbnail() {
if (thumbnail_)
thumbnail_->RequestThumbnailImage();
}
// content::WebContents:
void WebContentsDestroyed() override {
// We must un-observe each ThumbnailImage when the WebContents it came from
// closes.
if (thumbnail_) {
observer_.Remove(thumbnail_.get());
thumbnail_.reset();
}
// Destroy ourself. After this call, |this| doesn't exist!
parent_->ContentsClosed(web_contents());
}
// ThumbnailImage::Observer:
void OnThumbnailImageAvailable(gfx::ImageSkia thumbnail_image) override {
parent_->ThumbnailUpdated(web_contents(), thumbnail_image);
}
private:
ThumbnailTracker* parent_;
scoped_refptr<ThumbnailImage> thumbnail_;
ScopedObserver<ThumbnailImage, ContentsData> observer_;
};
ThumbnailTracker::ThumbnailTracker(ThumbnailUpdatedCallback callback)
: ThumbnailTracker(std::move(callback),
base::Bind(GetThumbnailFromTabHelper)) {}
ThumbnailTracker::ThumbnailTracker(ThumbnailUpdatedCallback callback,
GetThumbnailCallback thumbnail_getter)
: thumbnail_getter_(std::move(thumbnail_getter)),
callback_(std::move(callback)) {}
ThumbnailTracker::~ThumbnailTracker() = default;
void ThumbnailTracker::WatchTab(content::WebContents* contents) {
auto data_it = contents_data_.find(contents);
if (data_it == contents_data_.end()) {
data_it = contents_data_.emplace_hint(
data_it, contents, std::make_unique<ContentsData>(this, contents));
}
data_it->second->RequestThumbnail();
}
void ThumbnailTracker::ThumbnailUpdated(content::WebContents* contents,
gfx::ImageSkia image) {
callback_.Run(contents, image);
}
void ThumbnailTracker::ContentsClosed(content::WebContents* contents) {
contents_data_.erase(contents);
}
// static
scoped_refptr<ThumbnailImage> ThumbnailTracker::GetThumbnailFromTabHelper(
content::WebContents* contents) {
ThumbnailTabHelper* thumbnail_helper =
ThumbnailTabHelper::FromWebContents(contents);
// Gracefully handle when ThumbnailTabHelper isn't available.
if (thumbnail_helper) {
auto thumbnail = thumbnail_helper->thumbnail();
DCHECK(thumbnail);
return thumbnail;
} else {
DVLOG(1) << "ThumbnailTabHelper doesn't exist for tab";
return nullptr;
}
}
// Copyright 2019 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.
#ifndef CHROME_BROWSER_UI_WEBUI_TAB_STRIP_THUMBNAIL_TRACKER_H_
#define CHROME_BROWSER_UI_WEBUI_TAB_STRIP_THUMBNAIL_TRACKER_H_
#include <memory>
#include "base/containers/flat_map.h"
#include "base/memory/scoped_refptr.h"
#include "ui/gfx/image/image_skia.h"
namespace content {
class WebContents;
}
class ThumbnailImage;
// Tracks the thumbnails of a set of WebContentses. This set is dynamically
// managed, and WebContents closure is handled gracefully. The user is notified
// of any thumbnail change via a callback specified on construction.
class ThumbnailTracker {
public:
// Should return the ThumbnailImage instance for a WebContents.
using GetThumbnailCallback =
base::RepeatingCallback<scoped_refptr<ThumbnailImage>(
content::WebContents*)>;
// Handles a thumbnail update for a tab.
using ThumbnailUpdatedCallback =
base::RepeatingCallback<void(content::WebContents*, gfx::ImageSkia)>;
explicit ThumbnailTracker(ThumbnailUpdatedCallback callback);
// Specifies how to get a ThumbnailImage for a WebContents. This is intended
// for tests.
ThumbnailTracker(ThumbnailUpdatedCallback callback,
GetThumbnailCallback thumbnail_getter);
~ThumbnailTracker();
// Registers a tab to receive thumbnail updates for. Also immediately requests
// the current thumbnail.
void WatchTab(content::WebContents* contents);
private:
void ThumbnailUpdated(content::WebContents* contents, gfx::ImageSkia image);
void ContentsClosed(content::WebContents* contents);
// The default |GetThumbnailCallback| implementation.
static scoped_refptr<ThumbnailImage> GetThumbnailFromTabHelper(
content::WebContents* contents);
GetThumbnailCallback thumbnail_getter_;
ThumbnailUpdatedCallback callback_;
// ContentsData tracks a particular WebContents. One is created for a tab on
// its first thumbnail request and exists until the contents is closed.
class ContentsData;
base::flat_map<content::WebContents*, std::unique_ptr<ContentsData>>
contents_data_;
};
#endif // CHROME_BROWSER_UI_WEBUI_TAB_STRIP_THUMBNAIL_TRACKER_H_
// Copyright 2019 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 "chrome/browser/ui/webui/tab_strip/thumbnail_tracker.h"
#include <map>
#include <memory>
#include <utility>
#include "base/run_loop.h"
#include "base/test/mock_callback.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/sessions/session_tab_helper.h"
#include "chrome/browser/ui/thumbnails/thumbnail_image.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
namespace {
class TestThumbnailImageDelegate : public ThumbnailImage::Delegate {
public:
TestThumbnailImageDelegate() = default;
~TestThumbnailImageDelegate() override = default;
void ThumbnailImageBeingObservedChanged(bool is_being_observed) override {}
};
class ThumbnailTrackerTest : public ::testing::Test,
public ThumbnailImage::Delegate {
protected:
ThumbnailTrackerTest()
: thumbnail_tracker_(
thumbnail_updated_callback_.Get(),
base::Bind(&ThumbnailTrackerTest::GetTestingThumbnail,
base::Unretained(this))) {}
static SkBitmap CreateTestingBitmap() {
SkBitmap bitmap;
bitmap.allocN32Pixels(1, 1, true);
bitmap.eraseColor(SK_ColorBLACK);
bitmap.setImmutable();
return bitmap;
}
std::unique_ptr<content::WebContents> CreateWebContents() {
auto contents =
content::WebContentsTester::CreateTestWebContents(&profile_, nullptr);
SessionTabHelper::CreateForWebContents(contents.get());
return contents;
}
scoped_refptr<ThumbnailImage> GetTestingThumbnail(
content::WebContents* contents) {
return tab_thumbnails_[contents].thumbnail_image;
}
// ThumbnailImage::Delegate:
void ThumbnailImageBeingObservedChanged(bool is_being_observed) override {}
content::BrowserTaskEnvironment task_environment_;
TestingProfile profile_;
base::MockCallback<ThumbnailTracker::ThumbnailUpdatedCallback>
thumbnail_updated_callback_;
struct ThumbnailData {
ThumbnailData()
: thumbnail_image(base::MakeRefCounted<ThumbnailImage>(&delegate)) {}
TestThumbnailImageDelegate delegate;
scoped_refptr<ThumbnailImage> thumbnail_image;
};
std::map<content::WebContents*, ThumbnailData> tab_thumbnails_;
ThumbnailTracker thumbnail_tracker_;
};
} // namespace
using ::testing::_;
TEST_F(ThumbnailTrackerTest, WatchTabGetsCurrentThumbnail) {
auto contents = CreateWebContents();
auto thumbnail = GetTestingThumbnail(contents.get());
// Set the thumbnail image and wait for it to be stored.
base::RunLoop encode_loop;
thumbnail->set_async_operation_finished_callback_for_testing(
encode_loop.QuitClosure());
thumbnail->AssignSkBitmap(CreateTestingBitmap());
encode_loop.Run();
// Verify that WatchTab() gets the current image, waiting for the decoding to
// happen.
EXPECT_CALL(thumbnail_updated_callback_, Run(contents.get(), _)).Times(1);
base::RunLoop notify_loop;
thumbnail->set_async_operation_finished_callback_for_testing(
notify_loop.QuitClosure());
thumbnail_tracker_.WatchTab(contents.get());
notify_loop.Run();
}
TEST_F(ThumbnailTrackerTest, PropagatesThumbnailUpdate) {
auto contents1 = CreateWebContents();
auto thumbnail1 = GetTestingThumbnail(contents1.get());
auto contents2 = CreateWebContents();
auto thumbnail2 = GetTestingThumbnail(contents2.get());
// Since no thumbnail image exists yet, this shouldn't notify our callback.
thumbnail_tracker_.WatchTab(contents1.get());
thumbnail_tracker_.WatchTab(contents2.get());
{
::testing::InSequence seq;
EXPECT_CALL(thumbnail_updated_callback_, Run(contents1.get(), _)).Times(1);
EXPECT_CALL(thumbnail_updated_callback_, Run(contents2.get(), _)).Times(1);
}
base::RunLoop first_update_loop;
thumbnail1->set_async_operation_finished_callback_for_testing(
first_update_loop.QuitClosure());
thumbnail1->AssignSkBitmap(CreateTestingBitmap());
first_update_loop.Run();
base::RunLoop second_update_loop;
thumbnail2->set_async_operation_finished_callback_for_testing(
second_update_loop.QuitClosure());
thumbnail2->AssignSkBitmap(CreateTestingBitmap());
second_update_loop.Run();
}
TEST_F(ThumbnailTrackerTest, StopsObservingOnTabClose) {
auto contents = CreateWebContents();
auto thumbnail = GetTestingThumbnail(contents.get());
thumbnail_tracker_.WatchTab(contents.get());
// |thumbnail| is still valid, but |thumbnail_tracker_| should stop watching
// it when |contents| goes away.
EXPECT_CALL(thumbnail_updated_callback_, Run(_, _)).Times(0);
contents.reset();
base::RunLoop update_loop;
thumbnail->set_async_operation_finished_callback_for_testing(
update_loop.QuitClosure());
thumbnail->AssignSkBitmap(CreateTestingBitmap());
update_loop.Run();
}
...@@ -5150,6 +5150,10 @@ test("unit_tests") { ...@@ -5150,6 +5150,10 @@ test("unit_tests") {
"../browser/media/router/providers/openscreen/network_service_quic_packet_writer_unittest.cc", "../browser/media/router/providers/openscreen/network_service_quic_packet_writer_unittest.cc",
] ]
} }
if (enable_webui_tab_strip) {
sources += [ "../browser/ui/webui/tab_strip/thumbnail_tracker_unittest.cc" ]
}
} }
static_library("test_support_unit") { static_library("test_support_unit") {
......
...@@ -90,10 +90,6 @@ export function webUIResponse(id, isSuccess, response) { ...@@ -90,10 +90,6 @@ export function webUIResponse(id, isSuccess, response) {
} }
} }
// Expose |cr.webUIResponse| globally, since this is called directly from C++.
window.cr = window.cr || {};
window.cr.webUIResponse = webUIResponse;
/** /**
* A variation of chrome.send, suitable for messages that expect a single * A variation of chrome.send, suitable for messages that expect a single
* response from C++. * response from C++.
...@@ -174,6 +170,11 @@ export function removeWebUIListener(listener) { ...@@ -174,6 +170,11 @@ export function removeWebUIListener(listener) {
return false; return false;
} }
// Globally expose functions that must be called from C++.
window.cr = window.cr || {};
window.cr.webUIResponse = webUIResponse;
window.cr.webUIListenerCallback = webUIListenerCallback;
/** Whether we are using a Mac or not. */ /** Whether we are using a Mac or not. */
export const isMac = /Mac/.test(navigator.platform); export const isMac = /Mac/.test(navigator.platform);
......
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