diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/components/paint_preview | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-85-based.tar.gz |
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/paint_preview')
44 files changed, 1721 insertions, 247 deletions
diff --git a/chromium/components/paint_preview/browser/BUILD.gn b/chromium/components/paint_preview/browser/BUILD.gn index 90ebaf83615..3712dfeccb2 100644 --- a/chromium/components/paint_preview/browser/BUILD.gn +++ b/chromium/components/paint_preview/browser/BUILD.gn @@ -25,6 +25,7 @@ source_set("browser") { "paint_preview_compositor_service_impl.cc", "paint_preview_compositor_service_impl.h", "paint_preview_policy.h", + "service_sandbox_type.h", ] deps = [ diff --git a/chromium/components/paint_preview/browser/compositor_utils.cc b/chromium/components/paint_preview/browser/compositor_utils.cc index c8e342b96dc..120d2434685 100644 --- a/chromium/components/paint_preview/browser/compositor_utils.cc +++ b/chromium/components/paint_preview/browser/compositor_utils.cc @@ -7,9 +7,9 @@ #include <utility> #include "base/bind.h" -#include "base/task/post_task.h" #include "build/build_config.h" #include "components/discardable_memory/service/discardable_shared_memory_manager.h" +#include "components/paint_preview/browser/service_sandbox_type.h" #include "components/services/paint_preview_compositor/public/mojom/paint_preview_compositor.mojom.h" #include "components/strings/grit/components_strings.h" #include "content/public/browser/browser_task_traits.h" @@ -41,12 +41,10 @@ CreateCompositorCollection() { void CreateCompositorCollectionPending( mojo::PendingReceiver<mojom::PaintPreviewCompositorCollection> collection) { - // TODO(crbug/1074323): Investigate using a different SandboxType. content::ServiceProcessHost::Launch<mojom::PaintPreviewCompositorCollection>( std::move(collection), content::ServiceProcessHost::Options() .WithDisplayName(IDS_PAINT_PREVIEW_COMPOSITOR_SERVICE_DISPLAY_NAME) - .WithSandboxType(service_manager::SandboxType::kPrintCompositor) .Pass()); } @@ -56,8 +54,8 @@ void BindDiscardableSharedMemoryManager( discardable_memory_manager; // Set up the discardable memory manager. - base::PostTask( - FROM_HERE, {content::BrowserThread::IO}, + content::GetIOThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce( &BindDiscardableSharedMemoryManagerOnIOThread, discardable_memory_manager.InitWithNewPipeAndPassReceiver())); diff --git a/chromium/components/paint_preview/browser/directory_key.cc b/chromium/components/paint_preview/browser/directory_key.cc index 819e98c4bb9..45e07acdb7f 100644 --- a/chromium/components/paint_preview/browser/directory_key.cc +++ b/chromium/components/paint_preview/browser/directory_key.cc @@ -10,4 +10,8 @@ bool operator<(const DirectoryKey& a, const DirectoryKey& b) { return a.AsciiDirname() < b.AsciiDirname(); } +bool operator==(const DirectoryKey& a, const DirectoryKey& b) { + return a.AsciiDirname() == b.AsciiDirname(); +} + } // namespace paint_preview diff --git a/chromium/components/paint_preview/browser/directory_key.h b/chromium/components/paint_preview/browser/directory_key.h index a24b66afa29..bed4752f370 100644 --- a/chromium/components/paint_preview/browser/directory_key.h +++ b/chromium/components/paint_preview/browser/directory_key.h @@ -27,6 +27,8 @@ class DirectoryKey { bool operator<(const DirectoryKey& a, const DirectoryKey& b); +bool operator==(const DirectoryKey& a, const DirectoryKey& b); + } // namespace paint_preview #endif // COMPONENTS_PAINT_PREVIEW_BROWSER_DIRECTORY_KEY_H_ diff --git a/chromium/components/paint_preview/browser/file_manager.cc b/chromium/components/paint_preview/browser/file_manager.cc index 70fc611b3f2..8566302a2df 100644 --- a/chromium/components/paint_preview/browser/file_manager.cc +++ b/chromium/components/paint_preview/browser/file_manager.cc @@ -4,9 +4,14 @@ #include "components/paint_preview/browser/file_manager.h" +#include <algorithm> +#include <vector> + #include "base/files/file_enumerator.h" #include "base/files/file_util.h" #include "base/hash/hash.h" +#include "base/logging.h" +#include "base/metrics/histogram_functions.h" #include "base/strings/string_number_conversions.h" #include "components/paint_preview/common/file_utils.h" #include "third_party/zlib/google/zip.h" @@ -18,6 +23,11 @@ namespace { constexpr char kProtoName[] = "proto.pb"; constexpr char kZipExt[] = ".zip"; +bool CompareByLastModified(const base::FileEnumerator::FileInfo& a, + const base::FileEnumerator::FileInfo& b) { + return a.GetLastModifiedTime() < b.GetLastModifiedTime(); +} + } // namespace FileManager::FileManager( @@ -70,6 +80,10 @@ base::Optional<base::File::Info> FileManager::GetInfo( return info; } +size_t FileManager::GetTotalDiskUsage() const { + return base::ComputeDirectorySize(root_directory_); +} + bool FileManager::DirectoryExists(const DirectoryKey& key) const { DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); base::FilePath path; @@ -185,8 +199,23 @@ bool FileManager::SerializePaintPreviewProto(const DirectoryKey& key, auto path = CreateOrGetDirectory(key, false); if (!path.has_value()) return false; - return WriteProtoToFile(path->AppendASCII(kProtoName), proto) && - (!compress || CompressDirectory(key)); + bool result = WriteProtoToFile(path->AppendASCII(kProtoName), proto) && + (!compress || CompressDirectory(key)); + + if (compress) { + auto info = GetInfo(key); + if (info.has_value()) { + base::UmaHistogramMemoryKB( + "Browser.PaintPreview.Capture.CompressedOnDiskSize", + info->size / 1000); + } + } else { + size_t size_bytes = base::ComputeDirectorySize(path.value()); + base::UmaHistogramMemoryKB( + "Browser.PaintPreview.Capture.UncompressedOnDiskSize", + size_bytes / 1000); + } + return result; } std::unique_ptr<PaintPreviewProto> FileManager::DeserializePaintPreviewProto( @@ -212,6 +241,45 @@ base::flat_set<DirectoryKey> FileManager::ListUsedKeys() const { return base::flat_set<DirectoryKey>(std::move(keys)); } +std::vector<DirectoryKey> FileManager::GetOldestArtifactsForCleanup( + size_t max_size) { + // The rest of this function is expensive so cleanup should exit early if not + // required. + size_t size = base::ComputeDirectorySize(root_directory_); + if (size <= max_size) + return std::vector<DirectoryKey>(); + + base::FileEnumerator file_enum( + root_directory_, false, + base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES); + std::vector<base::FileEnumerator::FileInfo> file_infos; + for (base::FilePath name = file_enum.Next(); !name.empty(); + name = file_enum.Next()) { + file_infos.push_back(file_enum.GetInfo()); + } + + std::sort(file_infos.begin(), file_infos.end(), CompareByLastModified); + + std::vector<DirectoryKey> keys_to_remove; + for (const auto& file_info : file_infos) { + base::FilePath full_path = root_directory_.Append(file_info.GetName()); + + size_t size_delta = file_info.GetSize(); + // Computing a directory size is expensive. Most files should hopefully be + // compressed already. + if (file_info.IsDirectory()) + size_delta = base::ComputeDirectorySize(full_path); + + // Directory names should always be ASCII. + keys_to_remove.emplace_back( + file_info.GetName().RemoveExtension().MaybeAsASCII()); + size -= size_delta; + if (size <= max_size) + break; + } + return keys_to_remove; +} + FileManager::StorageType FileManager::GetPathForKey( const DirectoryKey& key, base::FilePath* path) const { diff --git a/chromium/components/paint_preview/browser/file_manager.h b/chromium/components/paint_preview/browser/file_manager.h index 3d9173e4ce4..126e418fd96 100644 --- a/chromium/components/paint_preview/browser/file_manager.h +++ b/chromium/components/paint_preview/browser/file_manager.h @@ -16,6 +16,7 @@ #include "url/gurl.h" namespace paint_preview { +class PaintPreviewTabService; // FileManager manages paint preview files associated with a root directory. // Typically the root directory is <profile_dir>/paint_previews/<feature>. @@ -47,6 +48,9 @@ class FileManager : public base::RefCountedThreadSafe<FileManager> { size_t GetSizeOfArtifacts(const DirectoryKey& key) const; base::Optional<base::File::Info> GetInfo(const DirectoryKey& key) const; + // Returns the total disk usage of all paint previews. + size_t GetTotalDiskUsage() const; + // Returns true if the directory for |key| exists. bool DirectoryExists(const DirectoryKey& key) const; @@ -87,10 +91,18 @@ class FileManager : public base::RefCountedThreadSafe<FileManager> { // Lists the current set of in-use DirectoryKeys. base::flat_set<DirectoryKey> ListUsedKeys() const; + // Returns a list of the least recently modified artifact sets until which + // when deleted would result in a total capture size on disk that is less than + // |max_size|. + std::vector<DirectoryKey> GetOldestArtifactsForCleanup(size_t max_size); + private: friend class base::RefCountedThreadSafe<FileManager>; ~FileManager(); + friend class PaintPreviewTabService; + base::FilePath GetPath() const { return root_directory_; } + enum StorageType { kNone = 0, kDirectory = 1, diff --git a/chromium/components/paint_preview/browser/file_manager_unittest.cc b/chromium/components/paint_preview/browser/file_manager_unittest.cc index cacb0ce55e8..0ee9043e5ca 100644 --- a/chromium/components/paint_preview/browser/file_manager_unittest.cc +++ b/chromium/components/paint_preview/browser/file_manager_unittest.cc @@ -138,6 +138,7 @@ TEST_F(FileManagerTest, TestCompression) { // Compress. base::FilePath zip_path = directory.AddExtensionASCII(".zip"); EXPECT_TRUE(manager->CompressDirectory(key)); + EXPECT_TRUE(manager->CompressDirectory(key)); // no-op EXPECT_GT(manager->GetSizeOfArtifacts(key), 0U); EXPECT_FALSE(base::PathExists(directory)); EXPECT_FALSE(base::PathExists(test_file)); @@ -275,10 +276,66 @@ TEST_F(FileManagerTest, HandleProtoCompressed) { metadata->set_url(GURL("www.chromium.org").spec()); EXPECT_TRUE(manager->SerializePaintPreviewProto(key, original_proto, true)); + EXPECT_TRUE(manager->CaptureExists(key)); + EXPECT_TRUE(base::PathExists(path.AddExtensionASCII(".zip"))); auto out_proto = manager->DeserializePaintPreviewProto(key); EXPECT_NE(out_proto, nullptr); EXPECT_THAT(*out_proto, EqualsProto(original_proto)); + + EXPECT_TRUE(manager->CaptureExists(key)); +} + +TEST_F(FileManagerTest, OldestFilesForCleanup) { + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + auto manager = + base::MakeRefCounted<FileManager>(temp_dir.GetPath(), MainTaskRunner()); + + const std::string data = "Foobar"; + + auto key_0 = manager->CreateKey(0U); + base::FilePath path_0 = + manager->CreateOrGetDirectory(key_0, true).value_or(base::FilePath()); + base::WriteFile(path_0.AppendASCII("0.txt"), data.data(), data.size()); + { + auto to_delete = manager->GetOldestArtifactsForCleanup(0U); + ASSERT_EQ(to_delete.size(), 1U); + EXPECT_EQ(to_delete[0], key_0); + } + { + auto to_delete = manager->GetOldestArtifactsForCleanup(50U); + EXPECT_EQ(to_delete.size(), 0U); + } + + auto key_1 = manager->CreateKey(1U); + base::FilePath path_1 = + manager->CreateOrGetDirectory(key_1, true).value_or(base::FilePath()); + base::WriteFile(path_1.AppendASCII("1.txt"), data.data(), data.size()); + manager->CompressDirectory(key_1); + base::Time modified_time = base::Time::Now(); + auto path_1_zip = path_1.AddExtensionASCII(".zip"); + base::TouchFile(path_0, modified_time - base::TimeDelta::FromSeconds(10), + modified_time - base::TimeDelta::FromSeconds(10)); + base::TouchFile(path_1_zip, modified_time, modified_time); + + { + auto to_delete = manager->GetOldestArtifactsForCleanup(0U); + ASSERT_EQ(to_delete.size(), 2U); + // Elements should be ordered in oldest to newest. + EXPECT_EQ(to_delete[0], key_0); + EXPECT_EQ(to_delete[1], key_1); + } + { + // Zip is ~116 bytes. + auto to_delete = manager->GetOldestArtifactsForCleanup(120U); + ASSERT_EQ(to_delete.size(), 1U); + EXPECT_EQ(to_delete[0], key_0); + } + { + auto to_delete = manager->GetOldestArtifactsForCleanup(150U); + EXPECT_EQ(to_delete.size(), 0U); + } } } // namespace paint_preview diff --git a/chromium/components/paint_preview/browser/paint_preview_base_service.cc b/chromium/components/paint_preview/browser/paint_preview_base_service.cc index 7b54b73c970..d99dce23ff4 100644 --- a/chromium/components/paint_preview/browser/paint_preview_base_service.cc +++ b/chromium/components/paint_preview/browser/paint_preview_base_service.cc @@ -148,7 +148,9 @@ void PaintPreviewBaseService::OnCaptured( if (web_contents) web_contents->DecrementCapturerCount(true); - if (status != mojom::PaintPreviewStatus::kOk || !proto) { + if (!(status == mojom::PaintPreviewStatus::kOk || + status == mojom::PaintPreviewStatus::kPartialSuccess) || + !proto) { DVLOG(1) << "ERROR: Paint Preview failed to capture for document " << guid.ToString() << " with error " << status; std::move(callback).Run(kCaptureFailed, nullptr); diff --git a/chromium/components/paint_preview/browser/paint_preview_client.cc b/chromium/components/paint_preview/browser/paint_preview_client.cc index 35dec779664..d18772758fd 100644 --- a/chromium/components/paint_preview/browser/paint_preview_client.cc +++ b/chromium/components/paint_preview/browser/paint_preview_client.cc @@ -6,6 +6,7 @@ #include <utility> +#include "base/files/file_util.h" #include "base/metrics/histogram_functions.h" #include "base/strings/strcat.h" #include "base/strings/string_number_conversions.h" @@ -57,6 +58,8 @@ PaintPreviewCaptureResponseToPaintPreviewFrameProto( PaintPreviewFrameProto* proto) { proto->set_embedding_token_high(frame_guid.GetHighForSerialization()); proto->set_embedding_token_low(frame_guid.GetLowForSerialization()); + proto->set_scroll_offset_x(response->scroll_offsets.width()); + proto->set_scroll_offset_y(response->scroll_offsets.height()); std::vector<base::UnguessableToken> frame_guids; for (const auto& id_pair : response->content_id_to_embedding_token) { @@ -332,9 +335,23 @@ void PaintPreviewClient::OnPaintPreviewCapturedCallback( // |status| MarkFrameAsProcessed(guid, frame_guid); - if (status == mojom::PaintPreviewStatus::kOk) + if (status == mojom::PaintPreviewStatus::kOk) { status = RecordFrame(guid, frame_guid, is_main_frame, filename, render_frame_id, std::move(response)); + } else { + // If the capture failed then cleanup the file. + base::ThreadPool::PostTask( + FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT}, + base::BindOnce(base::GetDeleteFileCallback(), filename)); + + // If this is the main frame we should just abort the capture. + if (is_main_frame) { + auto it = all_document_data_.find(guid); + if (it != all_document_data_.end()) + OnFinished(guid, &it->second); + } + } + auto it = all_document_data_.find(guid); if (it == all_document_data_.end()) return; diff --git a/chromium/components/paint_preview/browser/paint_preview_client_unittest.cc b/chromium/components/paint_preview/browser/paint_preview_client_unittest.cc index 91962004fd7..ad6126749bb 100644 --- a/chromium/components/paint_preview/browser/paint_preview_client_unittest.cc +++ b/chromium/components/paint_preview/browser/paint_preview_client_unittest.cc @@ -18,6 +18,7 @@ #include "content/public/browser/render_process_host.h" #include "content/public/browser/web_contents.h" #include "content/public/test/test_renderer_host.h" +#include "content/public/test/test_utils.h" #include "mojo/public/cpp/bindings/associated_receiver.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" @@ -138,11 +139,14 @@ TEST_F(PaintPreviewClientRenderViewHostTest, CaptureMainFrameMock) { auto response = mojom::PaintPreviewCaptureResponse::New(); response->embedding_token = base::nullopt; + response->scroll_offsets = gfx::Size(5, 10); PaintPreviewProto expected_proto; expected_proto.mutable_metadata()->set_url(expected_url.spec()); PaintPreviewFrameProto* main_frame = expected_proto.mutable_root_frame(); main_frame->set_is_main_frame(true); + main_frame->set_scroll_offset_x(5); + main_frame->set_scroll_offset_y(10); base::RunLoop loop; auto callback = base::BindOnce( @@ -223,6 +227,8 @@ TEST_F(PaintPreviewClientRenderViewHostTest, CaptureFailureMock) { ASSERT_NE(client, nullptr); client->CapturePaintPreview(params, main_rfh(), std::move(callback)); loop.Run(); + content::RunAllTasksUntilIdle(); + EXPECT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath())); } TEST_F(PaintPreviewClientRenderViewHostTest, RenderFrameDeletedNotCapturing) { diff --git a/chromium/components/paint_preview/browser/service_sandbox_type.h b/chromium/components/paint_preview/browser/service_sandbox_type.h new file mode 100644 index 00000000000..f955efc4a33 --- /dev/null +++ b/chromium/components/paint_preview/browser/service_sandbox_type.h @@ -0,0 +1,29 @@ +// Copyright 2020 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 COMPONENTS_PAINT_PREVIEW_BROWSER_SERVICE_SANDBOX_TYPE_H_ +#define COMPONENTS_PAINT_PREVIEW_BROWSER_SERVICE_SANDBOX_TYPE_H_ + +#include "content/public/browser/sandbox_type.h" +#include "content/public/browser/service_process_host.h" + +// This file maps service classes to sandbox types. Services which +// require a non-utility sandbox can be added here. See +// ServiceProcessHost::Launch() for how these templates are consumed. + +// paint_preview::mojom::PaintPreviewCompositorCollection +namespace paint_preview { +namespace mojom { +class PaintPreviewCompositorCollection; +} +} // namespace paint_preview + +template <> +inline content::SandboxType content::GetServiceSandboxType< + paint_preview::mojom::PaintPreviewCompositorCollection>() { + // TODO(crbug/1074323): Investigate using a different SandboxType. + return content::SandboxType::kPrintCompositor; +} + +#endif // COMPONENTS_PAINT_PREVIEW_BROWSER_SERVICE_SANDBOX_TYPE_H_ diff --git a/chromium/components/paint_preview/common/mojom/paint_preview_recorder.mojom b/chromium/components/paint_preview/common/mojom/paint_preview_recorder.mojom index 0b5d1bd8492..8201d243563 100644 --- a/chromium/components/paint_preview/common/mojom/paint_preview_recorder.mojom +++ b/chromium/components/paint_preview/common/mojom/paint_preview_recorder.mojom @@ -79,6 +79,9 @@ struct PaintPreviewCaptureResponse { // The size of the resulting serialized capture. uint64 serialized_size; + + // Scroll offsets of the frame at capture time. + gfx.mojom.Size scroll_offsets; }; // Service for capturing a paint preview of a RenderFrame's contents. This diff --git a/chromium/components/paint_preview/common/paint_preview_tracker.cc b/chromium/components/paint_preview/common/paint_preview_tracker.cc index 77a76ca19ea..dede9c035f7 100644 --- a/chromium/components/paint_preview/common/paint_preview_tracker.cc +++ b/chromium/components/paint_preview/common/paint_preview_tracker.cc @@ -44,14 +44,16 @@ PaintPreviewTracker::PaintPreviewTracker( bool is_main_frame) : guid_(guid), embedding_token_(embedding_token), - is_main_frame_(is_main_frame) {} + is_main_frame_(is_main_frame), + scroll_(SkISize::Make(0, 0)) {} PaintPreviewTracker::~PaintPreviewTracker() = default; uint32_t PaintPreviewTracker::CreateContentForRemoteFrame( const gfx::Rect& rect, const base::UnguessableToken& embedding_token) { sk_sp<SkPicture> pic = SkPicture::MakePlaceholder( - SkRect::MakeXYWH(rect.x(), rect.y(), rect.width(), rect.height())); + SkRect::MakeXYWH(rect.x() + scroll_.width(), rect.y() + scroll_.height(), + rect.width(), rect.height())); const uint32_t content_id = pic->uniqueID(); DCHECK(!base::Contains(content_id_to_embedding_token_, content_id)); content_id_to_embedding_token_[content_id] = embedding_token; @@ -59,6 +61,10 @@ uint32_t PaintPreviewTracker::CreateContentForRemoteFrame( return content_id; } +void PaintPreviewTracker::SetScrollForFrame(const SkISize& scroll) { + scroll_ = scroll; +} + void PaintPreviewTracker::AddGlyphs(const SkTextBlob* blob) { if (!blob) return; @@ -105,7 +111,7 @@ void PaintPreviewTracker::CustomDataToSkPictureCallback(SkCanvas* canvas, DCHECK(it != subframe_pics_.end()); SkRect rect = it->second->cullRect(); - SkMatrix matrix = SkMatrix::MakeTrans(rect.x(), rect.y()); + SkMatrix matrix = SkMatrix::Translate(rect.x(), rect.y()); canvas->drawPicture(it->second, &matrix, nullptr); } diff --git a/chromium/components/paint_preview/common/paint_preview_tracker.h b/chromium/components/paint_preview/common/paint_preview_tracker.h index f8993f48d06..0041603d721 100644 --- a/chromium/components/paint_preview/common/paint_preview_tracker.h +++ b/chromium/components/paint_preview/common/paint_preview_tracker.h @@ -50,6 +50,9 @@ class PaintPreviewTracker { const gfx::Rect& rect, const base::UnguessableToken& embedding_token); + // Sets the scroll position for the top layer frame. + void SetScrollForFrame(const SkISize& scroll); + // Adds the glyphs in |blob| to the glyph usage tracker for the |blob|'s // associated typface. void AddGlyphs(const SkTextBlob* blob); @@ -83,6 +86,8 @@ class PaintPreviewTracker { const base::Optional<base::UnguessableToken> embedding_token_; const bool is_main_frame_; + SkISize scroll_; + std::vector<mojom::LinkDataPtr> links_; PictureSerializationContext content_id_to_embedding_token_; TypefaceUsageMap typeface_glyph_usage_; diff --git a/chromium/components/paint_preview/common/paint_preview_tracker_unittest.cc b/chromium/components/paint_preview/common/paint_preview_tracker_unittest.cc index ba050b22acb..8575ba71c47 100644 --- a/chromium/components/paint_preview/common/paint_preview_tracker_unittest.cc +++ b/chromium/components/paint_preview/common/paint_preview_tracker_unittest.cc @@ -61,6 +61,33 @@ TEST(PaintPreviewTrackerTest, TestRemoteFramePlaceholderPicture) { // underlying private picture record. } +TEST(PaintPreviewTrackerTest, TestRemoteFramePlaceholderPictureWithScroll) { + const base::UnguessableToken kDocToken = base::UnguessableToken::Create(); + const base::UnguessableToken kEmbeddingToken = + base::UnguessableToken::Create(); + PaintPreviewTracker tracker(kDocToken, kEmbeddingToken, true); + tracker.SetScrollForFrame(SkISize::Make(10, 20)); + + const base::UnguessableToken kEmbeddingTokenChild = + base::UnguessableToken::Create(); + gfx::Rect rect(50, 40, 30, 20); + uint32_t content_id = + tracker.CreateContentForRemoteFrame(rect, kEmbeddingTokenChild); + PictureSerializationContext* context = + tracker.GetPictureSerializationContext(); + EXPECT_TRUE(context->count(content_id)); + EXPECT_EQ((*context)[content_id], kEmbeddingTokenChild); + + SkPictureRecorder recorder; + SkCanvas* canvas = recorder.beginRecording(100, 100); + tracker.CustomDataToSkPictureCallback(canvas, content_id); + sk_sp<SkPicture> pic = recorder.finishRecordingAsPicture(); + + // TODO(crbug/1009552): find a good way to check that a filler picture was + // actually inserted into |pic|. This is difficult without using the + // underlying private picture record. +} + TEST(PaintPreviewTrackerTest, TestGlyphRunList) { const base::UnguessableToken kEmbeddingToken = base::UnguessableToken::Create(); diff --git a/chromium/components/paint_preview/common/proto/paint_preview.proto b/chromium/components/paint_preview/common/proto/paint_preview.proto index 155be9dd555..3216065037c 100644 --- a/chromium/components/paint_preview/common/proto/paint_preview.proto +++ b/chromium/components/paint_preview/common/proto/paint_preview.proto @@ -33,7 +33,7 @@ message ContentIdEmbeddingTokenPairProto { } // A paint preview of a single frame. -// NEXT_TAG = 7 +// NEXT_TAG = 10 message PaintPreviewFrameProto { // The embedding token for this frame to its parent. Every frame other than // the main frame should have a non-zero value here. @@ -51,6 +51,10 @@ message PaintPreviewFrameProto { // A mapping between the content IDs of subframes and the |id| field. repeated ContentIdEmbeddingTokenPairProto content_id_to_embedding_tokens = 6; + + // Position information for this frame. + optional uint32 scroll_offset_x = 7; + optional uint32 scroll_offset_y = 8; } // Metadata for the capture. diff --git a/chromium/components/paint_preview/features/features.h b/chromium/components/paint_preview/features/features.h index ca6d840feb3..34953b0949f 100644 --- a/chromium/components/paint_preview/features/features.h +++ b/chromium/components/paint_preview/features/features.h @@ -26,7 +26,7 @@ extern const base::Feature kPaintPreviewDemo; // Used to enable the paint preview capture and show on startup for Android. If // enabled, paint previews for each tab are captured when a tab is hidden and -// are deleted when a tab is cloased. When a tab with a captured paint perview +// are deleted when a tab is closed. When a tab with a captured paint perview // is shown at startup and there is no cached page we will show the paint // preview. extern const base::Feature kPaintPreviewShowOnStartup; diff --git a/chromium/components/paint_preview/player/android/BUILD.gn b/chromium/components/paint_preview/player/android/BUILD.gn index 471b734016e..094a188622a 100644 --- a/chromium/components/paint_preview/player/android/BUILD.gn +++ b/chromium/components/paint_preview/player/android/BUILD.gn @@ -48,10 +48,12 @@ android_library("java") { sources = [ "java/src/org/chromium/components/paintpreview/player/LinkClickHandler.java", + "java/src/org/chromium/components/paintpreview/player/OverscrollHandler.java", "java/src/org/chromium/components/paintpreview/player/PaintPreviewFrame.java", "java/src/org/chromium/components/paintpreview/player/PlayerCompositorDelegate.java", "java/src/org/chromium/components/paintpreview/player/PlayerCompositorDelegateImpl.java", "java/src/org/chromium/components/paintpreview/player/PlayerManager.java", + "java/src/org/chromium/components/paintpreview/player/PlayerSwipeRefreshHandler.java", "java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapPainter.java", "java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameCoordinator.java", "java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameGestureDetector.java", @@ -66,7 +68,9 @@ android_library("java") { "//base:base_java", "//base:jni_java", "//components/paint_preview/browser/android:java", + "//third_party/android_swipe_refresh:android_swipe_refresh_java", "//ui/android:ui_java", + "//ui/android:ui_java_resources", "//url:gurl_java", ] } @@ -110,6 +114,7 @@ android_library("javatests") { "//content/public/test/android:content_java_test_support", "//third_party/android_support_test_runner:rules_java", "//third_party/android_support_test_runner:runner_java", + "//third_party/hamcrest:hamcrest_java", "//third_party/junit", "//third_party/ub-uiautomator:ub_uiautomator_java", "//ui/android:ui_java_test_support", @@ -152,5 +157,6 @@ junit_binary("paint_preview_junit_tests") { "//base:base_java", "//base:base_java_test_support", "//base:base_junit_test_support", + "//ui/android:ui_full_java", ] } diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/OverscrollHandler.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/OverscrollHandler.java new file mode 100644 index 00000000000..d3c60dad447 --- /dev/null +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/OverscrollHandler.java @@ -0,0 +1,33 @@ +// Copyright 2020 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. + +package org.chromium.components.paintpreview.player; + +/** + * Interface for handling overscroll events in the player. + */ +public interface OverscrollHandler { + /** + * Used to start an overscroll event. Returns true if it is able to be created/consumed. + */ + boolean start(); + + /** + * Updates the overscroll amount. + * + * @param yDelta The change in overscroll amount. Positive values indicate more overscrolling. + */ + void pull(float yDelta); + + /** + * Releases the overscroll event. This will trigger a refresh if a sufficient number and + * distance of {@link #pull} calls occurred. + */ + void release(); + + /** + * Resets the overscroll event if it was aborted. + */ + void reset(); +} diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PaintPreviewFrame.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PaintPreviewFrame.java index 383e5d8270b..84bd86665b0 100644 --- a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PaintPreviewFrame.java +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PaintPreviewFrame.java @@ -27,18 +27,27 @@ class PaintPreviewFrame { private PaintPreviewFrame[] mSubFrames; // The coordinates of the sub-frames relative to this frame. private Rect[] mSubFrameClips; + // The initial scroll position of this frame. + private int mInitialScrollX; + private int mInitialScrollY; - PaintPreviewFrame(UnguessableToken guid, int contentWidth, int contentHeight) { + PaintPreviewFrame(UnguessableToken guid, int contentWidth, int contentHeight, + int initialScrollX, int initialScrollY) { mGuid = guid; mContentWidth = contentWidth; mContentHeight = contentHeight; + mInitialScrollX = initialScrollX; + mInitialScrollY = initialScrollY; } private PaintPreviewFrame(UnguessableToken guid, int contentWidth, int contentHeight, - PaintPreviewFrame[] subFrames, Rect[] subFrameClips) { + int initialScrollX, int initialScrollY, PaintPreviewFrame[] subFrames, + Rect[] subFrameClips) { mGuid = guid; mContentWidth = contentWidth; mContentHeight = contentHeight; + mInitialScrollX = initialScrollX; + mInitialScrollY = initialScrollY; mSubFrames = subFrames; mSubFrameClips = subFrameClips; } @@ -63,6 +72,14 @@ class PaintPreviewFrame { return mContentHeight; } + int getInitialScrollX() { + return mInitialScrollX; + } + + int getInitialScrollY() { + return mInitialScrollY; + } + PaintPreviewFrame[] getSubFrames() { return mSubFrames; } @@ -107,7 +124,9 @@ class PaintPreviewFrame { @VisibleForTesting static PaintPreviewFrame createInstanceForTest(UnguessableToken guid, int contentWidth, - int contentHeight, PaintPreviewFrame[] subFrames, Rect[] subFrameClips) { - return new PaintPreviewFrame(guid, contentWidth, contentHeight, subFrames, subFrameClips); + int contentHeight, int initialScrollX, int initialScrollY, + PaintPreviewFrame[] subFrames, Rect[] subFrameClips) { + return new PaintPreviewFrame(guid, contentWidth, contentHeight, initialScrollX, + initialScrollY, subFrames, subFrameClips); } } diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerCompositorDelegateImpl.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerCompositorDelegateImpl.java index 9e3cbe51b7a..32e0625367e 100644 --- a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerCompositorDelegateImpl.java +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerCompositorDelegateImpl.java @@ -24,8 +24,8 @@ import javax.annotation.Nonnull; @JNINamespace("paint_preview") class PlayerCompositorDelegateImpl implements PlayerCompositorDelegate { interface CompositorListener { - void onCompositorReady(boolean safeToShow, UnguessableToken rootFrameGuid, - UnguessableToken[] frameGuids, int[] frameContentSize, int[] subFramesCount, + void onCompositorReady(UnguessableToken rootFrameGuid, UnguessableToken[] frameGuids, + int[] frameContentSize, int[] scrollOffsets, int[] subFramesCount, UnguessableToken[] subFrameGuids, int[] subFrameClipRects); } @@ -35,12 +35,13 @@ class PlayerCompositorDelegateImpl implements PlayerCompositorDelegate { PlayerCompositorDelegateImpl(NativePaintPreviewServiceProvider service, GURL url, String directoryKey, @Nonnull CompositorListener compositorListener, - @Nonnull LinkClickHandler linkClickHandler) { + @Nonnull LinkClickHandler linkClickHandler, Runnable compositorErrorCallback) { mCompositorListener = compositorListener; mLinkClickHandler = linkClickHandler; if (service != null && service.getNativeService() != 0) { - mNativePlayerCompositorDelegate = PlayerCompositorDelegateImplJni.get().initialize( - this, service.getNativeService(), url.getSpec(), directoryKey); + mNativePlayerCompositorDelegate = PlayerCompositorDelegateImplJni.get().initialize(this, + service.getNativeService(), url.getSpec(), directoryKey, + compositorErrorCallback); } // TODO(crbug.com/1021590): Handle initialization errors when // mNativePlayerCompositorDelegate == 0. @@ -49,14 +50,16 @@ class PlayerCompositorDelegateImpl implements PlayerCompositorDelegate { /** * Called by native when the Paint Preview compositor is ready. * - * @param safeToShow true if the native initialization of the Paint Preview player succeeded and - * the captured result matched the expected URL. * @param rootFrameGuid The GUID for the root frame. * @param frameGuids Contains all frame GUIDs that are in this hierarchy. * @param frameContentSize Contains the content size for each frame. In native, this is called * scroll extent. The order corresponds to {@code frameGuids}. The content width and height for * the ith frame in {@code frameGuids} are respectively in the {@code 2*i} and {@code 2*i+1} * indices of {@code frameContentSize}. + * @param scrollOffsets Contains the initial scroll offsets for each frame. The order + * corresponds to {@code frameGuids}. The offset in x and y for the ith frame in + * {@code frameGuids} are respectively in the {@code 2*i} and {@code 2*i+1} indices of + * {@code scrollOffsets}. * @param subFramesCount Contains the number of sub-frames for each frame. The order corresponds * to {@code frameGuids}. The number of sub-frames for the {@code i}th frame in {@code * frameGuids} is {@code subFramesCount[i]}. @@ -73,11 +76,11 @@ class PlayerCompositorDelegateImpl implements PlayerCompositorDelegate { * subFrameGuids[4*k+3]}, where {@code k} has the same value as above. */ @CalledByNative - void onCompositorReady(boolean safeToShow, UnguessableToken rootFrameGuid, - UnguessableToken[] frameGuids, int[] frameContentSize, int[] subFramesCount, + void onCompositorReady(UnguessableToken rootFrameGuid, UnguessableToken[] frameGuids, + int[] frameContentSize, int[] scrollOffsets, int[] subFramesCount, UnguessableToken[] subFrameGuids, int[] subFrameClipRects) { - mCompositorListener.onCompositorReady(safeToShow, rootFrameGuid, frameGuids, - frameContentSize, subFramesCount, subFrameGuids, subFrameClipRects); + mCompositorListener.onCompositorReady(rootFrameGuid, frameGuids, frameContentSize, + scrollOffsets, subFramesCount, subFrameGuids, subFrameClipRects); } @Override @@ -119,7 +122,7 @@ class PlayerCompositorDelegateImpl implements PlayerCompositorDelegate { @NativeMethods interface Natives { long initialize(PlayerCompositorDelegateImpl caller, long nativePaintPreviewBaseService, - String urlSpec, String directoryKey); + String urlSpec, String directoryKey, Runnable compositorErrorCallback); void destroy(long nativePlayerCompositorDelegateAndroid); void requestBitmap(long nativePlayerCompositorDelegateAndroid, UnguessableToken frameGuid, Callback<Bitmap> bitmapCallback, Runnable errorCallback, float scaleFactor, diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerManager.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerManager.java index 794b786c9b0..8ba8a396d08 100644 --- a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerManager.java +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerManager.java @@ -13,7 +13,6 @@ import android.widget.FrameLayout; import androidx.annotation.VisibleForTesting; -import org.chromium.base.Callback; import org.chromium.base.TraceEvent; import org.chromium.base.UnguessableToken; import org.chromium.components.paintpreview.browser.NativePaintPreviewServiceProvider; @@ -24,6 +23,7 @@ import java.util.HashMap; import java.util.Map; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * This is the only public class in this package and is hence the access point of this component for @@ -34,22 +34,44 @@ public class PlayerManager { private PlayerCompositorDelegateImpl mDelegate; private PlayerFrameCoordinator mRootFrameCoordinator; private FrameLayout mHostView; - private Callback<Boolean> mViewReadyCallback; + private Runnable mViewReadyCallback; private static final String sInitEvent = "paint_preview PlayerManager init"; + private PlayerSwipeRefreshHandler mPlayerSwipeRefreshHandler; + private boolean mIgnoreInitialScrollOffset; + /** + * Creates a new {@link PlayerManager}. + * @param url The url for the stored content that should be shown. + * @param nativePaintPreviewServiceProvider The native paint preview service. + * @param directoryKey The key for the directory storing the data. + * @param linkClickHandler Called with a url to trigger a navigation. + * @param refreshCallback Called when the paint preview should be refreshed. + * @param viewReadyCallback Called when the view is ready. Will not be called if compositorError + * is called prior to the view being ready. + * @param backgroundColor The color used for the background. + * @param compositorErrorCallback Called when the compositor has had an error (either during + * initialization or due to a disconnect). + * @param ignoreInitialScrollOffset If true the initial scroll state that is recorded at capture + * time is ignored. + */ public PlayerManager(GURL url, Context context, NativePaintPreviewServiceProvider nativePaintPreviewServiceProvider, String directoryKey, @Nonnull LinkClickHandler linkClickHandler, - Callback<Boolean> viewReadyCallback, int backgroundColor) { + @Nullable Runnable refreshCallback, Runnable viewReadyCallback, int backgroundColor, + Runnable compositorErrorCallback, boolean ignoreInitialScrollOffset) { TraceEvent.startAsync(sInitEvent, hashCode()); mContext = context; mDelegate = new PlayerCompositorDelegateImpl(nativePaintPreviewServiceProvider, url, - directoryKey, this::onCompositorReady, linkClickHandler); + directoryKey, this::onCompositorReady, linkClickHandler, compositorErrorCallback); mHostView = new FrameLayout(mContext); + if (refreshCallback != null) { + mPlayerSwipeRefreshHandler = new PlayerSwipeRefreshHandler(mContext, refreshCallback); + } mHostView.setLayoutParams( new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); mHostView.setBackgroundColor(backgroundColor); mViewReadyCallback = viewReadyCallback; + mIgnoreInitialScrollOffset = ignoreInitialScrollOffset; } /** @@ -57,26 +79,26 @@ public class PlayerManager { * method initializes a sub-component for each frame and adds the view for the root frame to * {@link #mHostView}. */ - private void onCompositorReady(boolean safeToShow, UnguessableToken rootFrameGuid, - UnguessableToken[] frameGuids, int[] frameContentSize, int[] subFramesCount, + private void onCompositorReady(UnguessableToken rootFrameGuid, UnguessableToken[] frameGuids, + int[] frameContentSize, int[] scrollOffsets, int[] subFramesCount, UnguessableToken[] subFrameGuids, int[] subFrameClipRects) { - if (!safeToShow) { - mViewReadyCallback.onResult(safeToShow); - TraceEvent.finishAsync(sInitEvent, hashCode()); - return; - } - PaintPreviewFrame rootFrame = buildFrameTreeHierarchy(rootFrameGuid, frameGuids, - frameContentSize, subFramesCount, subFrameGuids, subFrameClipRects); + frameContentSize, scrollOffsets, subFramesCount, subFrameGuids, subFrameClipRects, + mIgnoreInitialScrollOffset); mRootFrameCoordinator = new PlayerFrameCoordinator(mContext, mDelegate, rootFrame.getGuid(), - rootFrame.getContentWidth(), rootFrame.getContentHeight(), true); + rootFrame.getContentWidth(), rootFrame.getContentHeight(), + rootFrame.getInitialScrollX(), rootFrame.getInitialScrollY(), true, + mPlayerSwipeRefreshHandler); buildSubFrameCoordinators(mRootFrameCoordinator, rootFrame); mHostView.addView(mRootFrameCoordinator.getView(), new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + if (mPlayerSwipeRefreshHandler != null) { + mHostView.addView(mPlayerSwipeRefreshHandler.getView()); + } TraceEvent.finishAsync(sInitEvent, hashCode()); - mViewReadyCallback.onResult(safeToShow); + mViewReadyCallback.run(); } /** @@ -88,13 +110,16 @@ public class PlayerManager { */ @VisibleForTesting static PaintPreviewFrame buildFrameTreeHierarchy(UnguessableToken rootFrameGuid, - UnguessableToken[] frameGuids, int[] frameContentSize, int[] subFramesCount, - UnguessableToken[] subFrameGuids, int[] subFrameClipRects) { + UnguessableToken[] frameGuids, int[] frameContentSize, int[] scrollOffsets, + int[] subFramesCount, UnguessableToken[] subFrameGuids, int[] subFrameClipRects, + boolean ignoreInitialScrollOffset) { Map<UnguessableToken, PaintPreviewFrame> framesMap = new HashMap<>(); for (int i = 0; i < frameGuids.length; i++) { + int initalScrollX = ignoreInitialScrollOffset ? 0 : scrollOffsets[i * 2]; + int initalScrollY = ignoreInitialScrollOffset ? 0 : scrollOffsets[(i * 2) + 1]; framesMap.put(frameGuids[i], - new PaintPreviewFrame( - frameGuids[i], frameContentSize[i * 2], frameContentSize[(i * 2) + 1])); + new PaintPreviewFrame(frameGuids[i], frameContentSize[i * 2], + frameContentSize[(i * 2) + 1], initalScrollX, initalScrollY)); } int subFrameIdIndex = 0; @@ -130,9 +155,10 @@ public class PlayerManager { for (int i = 0; i < frame.getSubFrames().length; i++) { PaintPreviewFrame childFrame = frame.getSubFrames()[i]; - PlayerFrameCoordinator childCoordinator = - new PlayerFrameCoordinator(mContext, mDelegate, childFrame.getGuid(), - childFrame.getContentWidth(), childFrame.getContentHeight(), false); + PlayerFrameCoordinator childCoordinator = new PlayerFrameCoordinator(mContext, + mDelegate, childFrame.getGuid(), childFrame.getContentWidth(), + childFrame.getContentHeight(), childFrame.getInitialScrollX(), + childFrame.getInitialScrollY(), false, null); buildSubFrameCoordinators(childCoordinator, childFrame); frameCoordinator.addSubFrame(childCoordinator, frame.getSubFrameClips()[i]); } diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerSwipeRefreshHandler.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerSwipeRefreshHandler.java new file mode 100644 index 00000000000..3662b5f8560 --- /dev/null +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/PlayerSwipeRefreshHandler.java @@ -0,0 +1,82 @@ +// Copyright 2020 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. + +package org.chromium.components.paintpreview.player; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup.LayoutParams; + +import org.chromium.third_party.android.swiperefresh.SwipeRefreshLayout; + +import javax.annotation.Nonnull; + +/** + * A class for handling overscroll to refresh behavior for the Paint Preview player. This is based + * on the modified version of the Android compat library's SwipeRefreshLayout due to the Player's + * FrameLayout not behaving like a normal scrolling view. + */ +public class PlayerSwipeRefreshHandler implements OverscrollHandler { + // The duration of the refresh animation after a refresh signal. + private static final int STOP_REFRESH_ANIMATION_DELAY_MS = 500; + + // The modified AppCompat version of the refresh effect. + private SwipeRefreshLayout mSwipeRefreshLayout; + + // A handler to delegate refreshes event to. + private Runnable mRefreshCallback; + + /* + * Constructs a new instance of the handler. + * + * @param context The Context to create tha handler for. + * @param refreshCallback The handler that refresh events are delegated to. + */ + public PlayerSwipeRefreshHandler(Context context, @Nonnull Runnable refreshCallback) { + mRefreshCallback = refreshCallback; + mSwipeRefreshLayout = new SwipeRefreshLayout(context); + mSwipeRefreshLayout.setLayoutParams( + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + // Use the same colors as {@link org.chromium.chrome.browser.SwipeRefreshHandler}. + mSwipeRefreshLayout.setProgressBackgroundColorSchemeResource( + org.chromium.ui.R.color.default_bg_color_elev_2); + mSwipeRefreshLayout.setColorSchemeResources( + org.chromium.ui.R.color.default_control_color_active); + mSwipeRefreshLayout.setEnabled(true); + + mSwipeRefreshLayout.setOnRefreshListener(() -> { + mSwipeRefreshLayout.postDelayed(() -> { + mSwipeRefreshLayout.setRefreshing(false); + }, STOP_REFRESH_ANIMATION_DELAY_MS); + mRefreshCallback.run(); + }); + } + + /* + * Gets the view that contains the swipe to refresh animations. + */ + public View getView() { + return mSwipeRefreshLayout; + } + + @Override + public boolean start() { + return mSwipeRefreshLayout.start(); + } + + @Override + public void pull(float yDelta) { + mSwipeRefreshLayout.pull(yDelta); + } + + @Override + public void release() { + mSwipeRefreshLayout.release(true); + } + + @Override + public void reset() { + mSwipeRefreshLayout.reset(); + } +} diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameCoordinator.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameCoordinator.java index 275c31ab192..85d7d1dfc74 100644 --- a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameCoordinator.java +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameCoordinator.java @@ -7,13 +7,17 @@ package org.chromium.components.paintpreview.player.frame; import android.content.Context; import android.graphics.Rect; import android.view.View; -import android.widget.Scroller; +import android.view.ViewConfiguration; +import android.widget.OverScroller; import org.chromium.base.UnguessableToken; +import org.chromium.components.paintpreview.player.OverscrollHandler; import org.chromium.components.paintpreview.player.PlayerCompositorDelegate; import org.chromium.ui.modelutil.PropertyModel; import org.chromium.ui.modelutil.PropertyModelChangeProcessor; +import javax.annotation.Nullable; + /** * Sets up the view and the logic behind it for a Paint Preview frame. */ @@ -26,12 +30,18 @@ public class PlayerFrameCoordinator { * binds them together. */ public PlayerFrameCoordinator(Context context, PlayerCompositorDelegate compositorDelegate, - UnguessableToken frameGuid, int contentWidth, int contentHeight, - boolean canDetectZoom) { + UnguessableToken frameGuid, int contentWidth, int contentHeight, int initialScrollX, + int initialScrollY, boolean canDetectZoom, + @Nullable OverscrollHandler overscrollHandler) { PropertyModel model = new PropertyModel.Builder(PlayerFrameProperties.ALL_KEYS).build(); - mMediator = new PlayerFrameMediator(model, compositorDelegate, new Scroller(context), - frameGuid, contentWidth, contentHeight); + OverScroller scroller = new OverScroller(context); + scroller.setFriction(ViewConfiguration.getScrollFriction() / 2); + mMediator = new PlayerFrameMediator(model, compositorDelegate, scroller, frameGuid, + contentWidth, contentHeight, initialScrollX, initialScrollY); mView = new PlayerFrameView(context, canDetectZoom, mMediator); + if (overscrollHandler != null) { + mMediator.setOverscrollHandler(overscrollHandler); + } PropertyModelChangeProcessor.create(model, mView, PlayerFrameViewBinder::bind); } diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameGestureDetector.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameGestureDetector.java index 756a79d6d67..daf4b27ed32 100644 --- a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameGestureDetector.java +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameGestureDetector.java @@ -64,6 +64,9 @@ class PlayerFrameGestureDetector mScaleGestureDetector.onTouchEvent(event); } + if (event.getAction() == MotionEvent.ACTION_UP) { + mPlayerFrameViewDelegate.onRelease(); + } return mGestureDetector.onTouchEvent(event); } @@ -133,5 +136,9 @@ class PlayerFrameGestureDetector } @Override - public void onScaleEnd(ScaleGestureDetector detector) {} + public void onScaleEnd(ScaleGestureDetector detector) { + assert mCanDetectZoom; + mPlayerFrameViewDelegate.scaleFinished( + detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY()); + } } diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediator.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediator.java index 500369a6d28..21fe4fc8794 100644 --- a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediator.java +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediator.java @@ -5,15 +5,17 @@ package org.chromium.components.paintpreview.player.frame; import android.graphics.Bitmap; +import android.graphics.Matrix; import android.graphics.Rect; import android.os.Handler; import android.view.View; -import android.widget.Scroller; +import android.widget.OverScroller; import androidx.annotation.VisibleForTesting; import org.chromium.base.Callback; import org.chromium.base.UnguessableToken; +import org.chromium.components.paintpreview.player.OverscrollHandler; import org.chromium.components.paintpreview.player.PlayerCompositorDelegate; import org.chromium.ui.modelutil.PropertyModel; @@ -37,6 +39,8 @@ import java.util.Map; * </ul> */ class PlayerFrameMediator implements PlayerFrameViewDelegate { + private static final float MAX_SCALE_FACTOR = 5f; + /** The GUID associated with the frame that this class is representing. */ private final UnguessableToken mGuid; /** The content width inside this frame, at a scale factor of 1. */ @@ -65,7 +69,7 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { private final List<Rect> mVisibleSubFrameScaledRects = new ArrayList<>(); private final PropertyModel mModel; private final PlayerCompositorDelegate mCompositorDelegate; - private final Scroller mScroller; + private final OverScroller mScroller; private final Handler mScrollerHandler; /** The user-visible area for this frame. */ private final Rect mViewportRect = new Rect(); @@ -88,12 +92,24 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { final Map<Float, boolean[][]> mRequiredBitmaps = new HashMap<>(); /** The current scale factor. */ private float mScaleFactor; + private float mInitialScaleFactor; + private float mUncommittedScaleFactor = 0f; + @VisibleForTesting + final Matrix mViewportScaleMatrix = new Matrix(); + private final Matrix mBitmapScaleMatrix = new Matrix(); + + /** For swipe-to-refresh logic */ + private OverscrollHandler mOverscrollHandler; + private boolean mIsOverscrolling = false; + private float mOverscrollAmount = 0f; PlayerFrameMediator(PropertyModel model, PlayerCompositorDelegate compositorDelegate, - Scroller scroller, UnguessableToken frameGuid, int contentWidth, int contentHeight) { + OverScroller scroller, UnguessableToken frameGuid, int contentWidth, int contentHeight, + int initialScrollX, int initialScrollY) { mModel = model; mModel.set(PlayerFrameProperties.SUBFRAME_VIEWS, mVisibleSubFrameViews); mModel.set(PlayerFrameProperties.SUBFRAME_RECTS, mVisibleSubFrameScaledRects); + mModel.set(PlayerFrameProperties.SCALE_MATRIX, mBitmapScaleMatrix); mCompositorDelegate = compositorDelegate; mScroller = scroller; @@ -101,6 +117,8 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { mContentWidth = contentWidth; mContentHeight = contentHeight; mScrollerHandler = new Handler(); + mViewportRect.offset(initialScrollX, initialScrollY); + mViewportScaleMatrix.postTranslate(-initialScrollX, -initialScrollY); } /** @@ -116,16 +134,62 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { @Override public void setLayoutDimensions(int width, int height) { + // Don't trigger a re-draw if we are actively scaling. + if (!mBitmapScaleMatrix.isIdentity()) { + mViewportRect.set(mViewportRect.left, mViewportRect.top, mViewportRect.left + width, + mViewportRect.top + height); + // Set scale factor to 0 so subframes get the correct scale factor on scale completion. + mScaleFactor = 0; + return; + } + // Set initial scale so that content width fits within the layout dimensions. - float initialScaleFactor = ((float) width) / ((float) mContentWidth); - updateViewportSize(width, height, mScaleFactor == 0f ? initialScaleFactor : mScaleFactor); + mInitialScaleFactor = ((float) width) / ((float) mContentWidth); + updateViewportSize( + width, height, (mScaleFactor == 0f) ? mInitialScaleFactor : mScaleFactor); + } + + @Override + public void setBitmapScaleMatrix(Matrix matrix, float scaleFactor) { + // Don't update the subframes if the matrix is identity as it will be forcibly recalculated. + if (!matrix.isIdentity()) { + updateSubFrames(mViewportRect, scaleFactor); + } + setBitmapScaleMatrixInternal(matrix, scaleFactor); + } + + private void setBitmapScaleMatrixInternal(Matrix matrix, float scaleFactor) { + float[] matrixValues = new float[9]; + matrix.getValues(matrixValues); + mBitmapScaleMatrix.setValues(matrixValues); + Matrix childBitmapScaleMatrix = new Matrix(); + childBitmapScaleMatrix.setScale( + matrixValues[Matrix.MSCALE_X], matrixValues[Matrix.MSCALE_Y]); + for (View subFrameView : mVisibleSubFrameViews) { + ((PlayerFrameView) subFrameView) + .updateDelegateScaleMatrix(childBitmapScaleMatrix, scaleFactor); + } + mModel.set(PlayerFrameProperties.SCALE_MATRIX, mBitmapScaleMatrix); + } + + @Override + public void forceRedraw() { + mInitialScaleFactor = ((float) mViewportRect.width()) / ((float) mContentWidth); + moveViewport(0, 0, (mScaleFactor == 0f) ? mInitialScaleFactor : mScaleFactor); + for (View subFrameView : mVisibleSubFrameViews) { + ((PlayerFrameView) subFrameView).forceRedraw(); + } } void updateViewportSize(int width, int height, float scaleFactor) { if (width <= 0 || height <= 0) return; - mViewportRect.set(mViewportRect.left, mViewportRect.top, mViewportRect.left + width, - mViewportRect.top + height); + // Ensure the viewport is within the bounds of the content. + final int left = Math.max( + 0, Math.min(mViewportRect.left, Math.round(mContentWidth * scaleFactor) - width)); + final int top = Math.max( + 0, Math.min(mViewportRect.top, Math.round(mContentHeight * scaleFactor) - height)); + mViewportRect.set(left, top, left + width, top + height); moveViewport(0, 0, scaleFactor); } @@ -157,16 +221,13 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { mRequiredBitmaps.put(scaleFactor, requiredBitmaps); } - // If the scale factor is changed, the view should get the correct bitmap matrix. - if (scaleFactor != mScaleFactor) { - mModel.set(PlayerFrameProperties.BITMAP_MATRIX, mBitmapMatrix.get(scaleFactor)); - mScaleFactor = scaleFactor; - } - // Update mViewportRect and let the view know. PropertyModelChangeProcessor is smart about // this and will only update the view if mViewportRect is actually changed. mViewportRect.offset(distanceX, distanceY); - updateSubFrames(); + // Keep translations up to date for the viewport matrix so that future scale operations are + // correct. + mViewportScaleMatrix.postTranslate(-distanceX, -distanceY); + updateSubFrames(mViewportRect, scaleFactor); mModel.set(PlayerFrameProperties.TILE_DIMENSIONS, tileDimensions); mModel.set(PlayerFrameProperties.VIEWPORT, mViewportRect); @@ -180,10 +241,12 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { // Request bitmaps for tiles inside the view port that don't already have a bitmap. final int tileWidth = tileDimensions[0]; final int tileHeight = tileDimensions[1]; - final int colStart = mViewportRect.left / tileWidth; - final int colEnd = (int) Math.ceil((double) mViewportRect.right / tileWidth); - final int rowStart = mViewportRect.top / tileHeight; - final int rowEnd = (int) Math.ceil((double) mViewportRect.bottom / tileHeight); + final int colStart = Math.max(0, (int) Math.floor((double) mViewportRect.left / tileWidth)); + final int colEnd = Math.min(requiredBitmaps[0].length, + (int) Math.ceil((double) mViewportRect.right / tileWidth)); + final int rowStart = Math.max(0, (int) Math.floor((double) mViewportRect.top / tileHeight)); + final int rowEnd = Math.min(requiredBitmaps.length, + (int) Math.ceil((double) mViewportRect.bottom / tileHeight)); for (int col = colStart; col < colEnd; col++) { for (int row = rowStart; row < rowEnd; row++) { int tileLeft = col * tileWidth; @@ -201,17 +264,27 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { requestBitmapForAdjacentTiles(tileWidth, tileHeight, row, col, scaleFactor); } } + + // If the scale factor is changed, the view should get the correct bitmap matrix. + // TODO(crbug/1090804): "Double buffer" this such that there is no period where there is a + // blank screen between scale finishing and new bitmaps being fetched. + if (scaleFactor != mScaleFactor) { + mModel.set(PlayerFrameProperties.BITMAP_MATRIX, mBitmapMatrix.get(scaleFactor)); + mBitmapMatrix.remove(mScaleFactor); // Eagerly free to avoid OOM. + mRequiredBitmaps.remove(mScaleFactor); + mScaleFactor = scaleFactor; + } } - private void updateSubFrames() { + private void updateSubFrames(Rect viewport, float scaleFactor) { mVisibleSubFrameViews.clear(); mVisibleSubFrameScaledRects.clear(); for (int i = 0; i < mSubFrameRects.size(); i++) { Rect subFrameScaledRect = mSubFrameScaledRects.get(i); - scaleRect(mSubFrameRects.get(i), subFrameScaledRect, mScaleFactor); - if (Rect.intersects(subFrameScaledRect, mViewportRect)) { - int transformedLeft = subFrameScaledRect.left - mViewportRect.left; - int transformedTop = subFrameScaledRect.top - mViewportRect.top; + scaleRect(mSubFrameRects.get(i), subFrameScaledRect, scaleFactor); + if (Rect.intersects(subFrameScaledRect, viewport)) { + int transformedLeft = subFrameScaledRect.left - viewport.left; + int transformedTop = subFrameScaledRect.top - viewport.top; subFrameScaledRect.set(transformedLeft, transformedTop, transformedLeft + subFrameScaledRect.width(), transformedTop + subFrameScaledRect.height()); @@ -278,6 +351,8 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { private void deleteUnrequiredBitmaps(float scaleFactor) { Bitmap[][] bitmapMatrix = mBitmapMatrix.get(scaleFactor); boolean[][] requiredBitmaps = mRequiredBitmaps.get(scaleFactor); + if (bitmapMatrix == null || requiredBitmaps == null) return; + for (int row = 0; row < bitmapMatrix.length; row++) { for (int col = 0; col < bitmapMatrix[row].length; col++) { Bitmap bitmap = bitmapMatrix[row][col]; @@ -299,10 +374,46 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { @Override public boolean scrollBy(float distanceX, float distanceY) { mScroller.forceFinished(true); + return scrollByInternal(distanceX, distanceY); } + @Override + public void onRelease() { + if (mOverscrollHandler == null || !mIsOverscrolling) return; + + mOverscrollHandler.release(); + mIsOverscrolling = false; + mOverscrollAmount = 0.0f; + } + + private boolean maybeHandleOverscroll(float distanceY) { + if (mOverscrollHandler == null || mViewportRect.top != 0) return false; + + // Ignore if there is no active overscroll and the direction is down. + if (!mIsOverscrolling && distanceY <= 0) return false; + + mOverscrollAmount += distanceY; + + // If the overscroll is completely eased off the cancel the event. + if (mOverscrollAmount <= 0) { + mIsOverscrolling = false; + mOverscrollHandler.reset(); + return false; + } + + // Start the overscroll event if the scroll direction is correct and one isn't active. + if (!mIsOverscrolling && distanceY > 0) { + mOverscrollAmount = distanceY; + mIsOverscrolling = mOverscrollHandler.start(); + } + mOverscrollHandler.pull(distanceY); + return mIsOverscrolling; + } + private boolean scrollByInternal(float distanceX, float distanceY) { + if (maybeHandleOverscroll(-distanceY)) return true; + int validDistanceX = 0; int validDistanceY = 0; float scaledContentWidth = mContentWidth * mScaleFactor; @@ -319,16 +430,147 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { validDistanceY = (int) Math.min(distanceY, scaledContentHeight - mViewportRect.bottom); } - if (validDistanceX == 0 && validDistanceY == 0) return false; + if (validDistanceX == 0 && validDistanceY == 0) { + return false; + } moveViewport(validDistanceX, validDistanceY, mScaleFactor); return true; } + /** + * How scale for the paint preview player works. + * + * There are two reference frames: + * - The currently loaded bitmaps, which changes as scaling happens. + * - The viewport, which is static until scaling is finished. + * + * During {@link #scaleBy()} the gesture is still ongoing. + * + * On each scale gesture the |scaleFactor| is applied to |mUncommittedScaleFactor| which + * accumulates the scale starting from the currently committed |mScaleFactor|. This is + * committed when {@link #scaleFinished()} event occurs. This is for the viewport reference + * frame. |mViewportScaleMatrix| also accumulates the transforms to track the translation + * behavior. + * + * |mBitmapScaleMatrix| tracks scaling from the perspective of the bitmaps. This is used to + * transform the canvas the bitmaps are painted on such that scaled images can be shown + * mid-gesture. + * + * Each subframe is updated with a new rect based on the interim scale factor and when the + * matrix is set in {@link PlayerFrameView#updateMatrix} the subframe matricies are recursively + * updated. + * + * On {@link #scaleFinished()} the gesture is now considered finished. + * + * The values of |mViewportScaleMatrix| are extracted to get the final translation applied to + * the viewport. The transform for the bitmaps (that is |mBitmapScaleMatrix|) is cancelled. + * + * During {@link #moveViewport()} new bitmaps are requested for the main frame and subframes + * to improve quality. + * + * NOTE: |mViewportScaleMatrix| also need to consume scroll events in order to keep track of + * the correct translation. + */ @Override public boolean scaleBy(float scaleFactor, float focalPointX, float focalPointY) { - // TODO(crbug.com/1020702): Add support for zooming. - return false; + // This is filtered to only apply to the top level view upstream. + if (mUncommittedScaleFactor == 0f) { + mUncommittedScaleFactor = mScaleFactor; + } + mUncommittedScaleFactor *= scaleFactor; + + // Don't scale outside of the acceptable range. The value is still accumulated such that the + // continuous gesture feels smooth. + if (mUncommittedScaleFactor < mInitialScaleFactor) return true; + if (mUncommittedScaleFactor > MAX_SCALE_FACTOR) return true; + + // TODO(crbug/1090804): trigger a fetch of new bitmaps periodically when zooming out. + + mViewportScaleMatrix.postScale(scaleFactor, scaleFactor, focalPointX, focalPointY); + mBitmapScaleMatrix.postScale(scaleFactor, scaleFactor, focalPointX, focalPointY); + + float[] viewportScaleMatrixValues = new float[9]; + mViewportScaleMatrix.getValues(viewportScaleMatrixValues); + + float[] bitmapScaleMatrixValues = new float[9]; + mBitmapScaleMatrix.getValues(bitmapScaleMatrixValues); + + // It is possible the scale pushed the viewport outside the content bounds. These new values + // are forced to be within bounds. + final float newX = -1f + * Math.max(0f, + Math.min(-viewportScaleMatrixValues[Matrix.MTRANS_X], + mContentWidth * mUncommittedScaleFactor - mViewportRect.width())); + final float newY = -1f + * Math.max(0f, + Math.min(-viewportScaleMatrixValues[Matrix.MTRANS_Y], + mContentHeight * mUncommittedScaleFactor - mViewportRect.height())); + final int newXRounded = Math.abs(Math.round(newX)); + final int newYRounded = Math.abs(Math.round(newY)); + updateSubFrames(new Rect(newXRounded, newYRounded, newXRounded + mViewportRect.width(), + newYRounded + mViewportRect.height()), + mUncommittedScaleFactor); + + if (newX != viewportScaleMatrixValues[Matrix.MTRANS_X] + || newY != viewportScaleMatrixValues[Matrix.MTRANS_Y]) { + // This is the delta required to force the viewport to be inside the bounds of the + // content. + final float deltaX = newX - viewportScaleMatrixValues[Matrix.MTRANS_X]; + final float deltaY = newY - viewportScaleMatrixValues[Matrix.MTRANS_Y]; + + // Directly used the forced bounds of the viewport reference frame for the viewport + // scale matrix. + viewportScaleMatrixValues[Matrix.MTRANS_X] = newX; + viewportScaleMatrixValues[Matrix.MTRANS_Y] = newY; + mViewportScaleMatrix.setValues(viewportScaleMatrixValues); + + // For the bitmap matrix we only want the delta as its position will be different as the + // coordinates are bitmap relative. + bitmapScaleMatrixValues[Matrix.MTRANS_X] += deltaX; + bitmapScaleMatrixValues[Matrix.MTRANS_Y] += deltaY; + mBitmapScaleMatrix.setValues(bitmapScaleMatrixValues); + } + setBitmapScaleMatrixInternal(mBitmapScaleMatrix, mUncommittedScaleFactor); + + return true; + } + + @Override + public boolean scaleFinished(float scaleFactor, float focalPointX, float focalPointY) { + // Remove the bitmap scaling to avoid issues when new bitmaps are requested. + // TODO(crbug/1090804): Defer clearing this so that double buffering can occur. + mBitmapScaleMatrix.reset(); + setBitmapScaleMatrixInternal(mBitmapScaleMatrix, 1f); + + final float finalScaleFactor = + Math.max(mInitialScaleFactor, Math.min(mUncommittedScaleFactor, MAX_SCALE_FACTOR)); + mUncommittedScaleFactor = 0f; + + float[] matrixValues = new float[9]; + mViewportScaleMatrix.getValues(matrixValues); + final float newX = Math.max(0f, + Math.min(-matrixValues[Matrix.MTRANS_X], + mContentWidth * finalScaleFactor - mViewportRect.width())); + final float newY = Math.max(0f, + Math.min(-matrixValues[Matrix.MTRANS_Y], + mContentHeight * finalScaleFactor - mViewportRect.height())); + matrixValues[Matrix.MTRANS_X] = -newX; + matrixValues[Matrix.MTRANS_Y] = -newY; + matrixValues[Matrix.MSCALE_X] = finalScaleFactor; + matrixValues[Matrix.MSCALE_Y] = finalScaleFactor; + mViewportScaleMatrix.setValues(matrixValues); + final int newXRounded = Math.round(newX); + final int newYRounded = Math.round(newY); + mViewportRect.set(newXRounded, newYRounded, newXRounded + mViewportRect.width(), + newYRounded + mViewportRect.height()); + + moveViewport(0, 0, finalScaleFactor); + for (View subFrameView : mVisibleSubFrameViews) { + ((PlayerFrameView) subFrameView).forceRedraw(); + } + + return true; } @Override @@ -353,6 +595,10 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { return true; } + public void setOverscrollHandler(OverscrollHandler overscrollHandler) { + mOverscrollHandler = overscrollHandler; + } + /** * Handles a fling update by computing the next scroll offset and programmatically scrolling. */ @@ -389,16 +635,25 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate { */ @Override public void onResult(Bitmap result) { - assert mBitmapMatrix.get(mRequestScaleFactor) != null; + if (result == null) { + run(); + return; + } + Bitmap[][] bitmapMatrix = mBitmapMatrix.get(mRequestScaleFactor); + boolean[][] requiredBitmaps = mRequiredBitmaps.get(mRequestScaleFactor); + if (bitmapMatrix == null || requiredBitmaps == null) { + result.recycle(); + return; + } + assert mBitmapMatrix.get(mRequestScaleFactor)[mRequestRow][mRequestCol] == null; assert mPendingBitmapRequests.get(mRequestScaleFactor)[mRequestRow][mRequestCol]; mPendingBitmapRequests.get(mScaleFactor)[mRequestRow][mRequestCol] = false; - if (mRequiredBitmaps.get(mRequestScaleFactor)[mRequestRow][mRequestCol]) { - mBitmapMatrix.get(mScaleFactor)[mRequestRow][mRequestCol] = result; + if (requiredBitmaps[mRequestRow][mRequestCol]) { + bitmapMatrix[mRequestRow][mRequestCol] = result; if (PlayerFrameMediator.this.mScaleFactor == mRequestScaleFactor) { - mModel.set( - PlayerFrameProperties.BITMAP_MATRIX, mBitmapMatrix.get(mScaleFactor)); + mModel.set(PlayerFrameProperties.BITMAP_MATRIX, bitmapMatrix); } } else { result.recycle(); diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameProperties.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameProperties.java index 6404d7f7459..e8f825d4f28 100644 --- a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameProperties.java +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameProperties.java @@ -5,6 +5,7 @@ package org.chromium.components.paintpreview.player.frame; import android.graphics.Bitmap; +import android.graphics.Matrix; import android.graphics.Rect; import android.view.View; @@ -39,6 +40,9 @@ class PlayerFrameProperties { */ static final PropertyModel.WritableObjectPropertyKey<List<Rect>> SUBFRAME_RECTS = new PropertyModel.WritableObjectPropertyKey<>(); + /** The matrix to apply to the view before a zoom is committed. */ + static final PropertyModel.WritableObjectPropertyKey<Matrix> SCALE_MATRIX = + new PropertyModel.WritableObjectPropertyKey<>(true); static final PropertyKey[] ALL_KEYS = { - BITMAP_MATRIX, TILE_DIMENSIONS, VIEWPORT, SUBFRAME_VIEWS, SUBFRAME_RECTS}; + BITMAP_MATRIX, TILE_DIMENSIONS, VIEWPORT, SUBFRAME_VIEWS, SUBFRAME_RECTS, SCALE_MATRIX}; } diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameView.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameView.java index bfd47130bae..1d47081ef66 100644 --- a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameView.java +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameView.java @@ -7,6 +7,7 @@ package org.chromium.components.paintpreview.player.frame; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Matrix; import android.graphics.Rect; import android.view.MotionEvent; import android.view.View; @@ -28,6 +29,7 @@ class PlayerFrameView extends FrameLayout { private PlayerFrameViewDelegate mDelegate; private List<View> mSubFrameViews; private List<Rect> mSubFrameRects; + private Matrix mScaleMatrix; /** * @param context Used for initialization. @@ -72,6 +74,47 @@ class PlayerFrameView extends FrameLayout { void updateViewPort(int left, int top, int right, int bottom) { mBitmapPainter.updateViewPort(left, top, right, bottom); + layoutSubframes(); + } + + void updateBitmapMatrix(Bitmap[][] bitmapMatrix) { + mBitmapPainter.updateBitmapMatrix(bitmapMatrix); + } + + void updateTileDimensions(int[] tileDimensions) { + mBitmapPainter.updateTileDimensions(tileDimensions); + } + + void updateScaleMatrix(Matrix matrix) { + mScaleMatrix = matrix; + if (mScaleMatrix.isIdentity()) return; + + postInvalidate(); + layoutSubframes(); + } + + void updateDelegateScaleMatrix(Matrix matrix, float scaleFactor) { + mDelegate.setBitmapScaleMatrix(matrix, scaleFactor); + } + + void forceRedraw() { + mDelegate.forceRedraw(); + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.save(); + canvas.setMatrix(mScaleMatrix); + mBitmapPainter.onDraw(canvas); + canvas.restore(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return mGestureDetector.onTouchEvent(event) || super.onTouchEvent(event); + } + + private void layoutSubframes() { // Remove all views if there are no sub-frames. if (mSubFrameViews == null || mSubFrameRects == null) { removeAllViews(); @@ -97,22 +140,4 @@ class PlayerFrameView extends FrameLayout { } } } - - void updateBitmapMatrix(Bitmap[][] bitmapMatrix) { - mBitmapPainter.updateBitmapMatrix(bitmapMatrix); - } - - void updateTileDimensions(int[] tileDimensions) { - mBitmapPainter.updateTileDimensions(tileDimensions); - } - - @Override - protected void onDraw(Canvas canvas) { - mBitmapPainter.onDraw(canvas); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - return mGestureDetector.onTouchEvent(event) || super.onTouchEvent(event); - } } diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameViewBinder.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameViewBinder.java index d18f49d0f10..2ae8bc0c451 100644 --- a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameViewBinder.java +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameViewBinder.java @@ -25,6 +25,8 @@ class PlayerFrameViewBinder { view.updateSubFrameViews(model.get(PlayerFrameProperties.SUBFRAME_VIEWS)); } else if (key.equals(PlayerFrameProperties.SUBFRAME_RECTS)) { view.updateSubFrameRects(model.get(PlayerFrameProperties.SUBFRAME_RECTS)); + } else if (key.equals(PlayerFrameProperties.SCALE_MATRIX)) { + view.updateScaleMatrix(model.get(PlayerFrameProperties.SCALE_MATRIX)); } } } diff --git a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameViewDelegate.java b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameViewDelegate.java index f671c1ca108..3b43a9c6dd5 100644 --- a/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameViewDelegate.java +++ b/chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameViewDelegate.java @@ -4,6 +4,8 @@ package org.chromium.components.paintpreview.player.frame; +import android.graphics.Matrix; + /** * Used by {@link PlayerFrameView} to delegate view events to {@link PlayerFrameMediator}. */ @@ -12,6 +14,17 @@ interface PlayerFrameViewDelegate { * Called on layout with the attributed width and height. */ void setLayoutDimensions(int width, int height); + + /** + * Called to set the bitmap scale matrix for this frame. + */ + void setBitmapScaleMatrix(Matrix matrix, float scaleFactor); + + /** + * Triggers a redraw if one is needed. + */ + void forceRedraw(); + /** * Called when a scroll gesture is performed. * @param distanceX Horizontal scroll values in pixels. @@ -19,6 +32,7 @@ interface PlayerFrameViewDelegate { * @return Whether this scroll event was consumed. */ boolean scrollBy(float distanceX, float distanceY); + /** * Called when a scale gesture is performed. * @return Whether this scale event was consumed. @@ -26,6 +40,12 @@ interface PlayerFrameViewDelegate { boolean scaleBy(float scaleFactor, float focalPointX, float focalPointY); /** + * Called when a scale gesture is finished. + * @return Whether this scale event was consumed. + */ + boolean scaleFinished(float scaleFactor, float focalPointX, float focalPointY); + + /** * Called when a single tap gesture is performed. * @param x X coordinate of the point clicked. * @param y Y coordinate of the point clicked. @@ -39,4 +59,9 @@ interface PlayerFrameViewDelegate { * @return Whether this fling was consumed. */ boolean onFling(float velocityX, float velocityY); + + /** + * Called when a gesture is released. + */ + void onRelease(); } diff --git a/chromium/components/paint_preview/player/android/javatests/src/org/chromium/components/paintpreview/player/PaintPreviewPlayerTest.java b/chromium/components/paint_preview/player/android/javatests/src/org/chromium/components/paintpreview/player/PaintPreviewPlayerTest.java index 8e52536bc7b..18066e021a1 100644 --- a/chromium/components/paint_preview/player/android/javatests/src/org/chromium/components/paintpreview/player/PaintPreviewPlayerTest.java +++ b/chromium/components/paint_preview/player/android/javatests/src/org/chromium/components/paintpreview/player/PaintPreviewPlayerTest.java @@ -6,11 +6,13 @@ package org.chromium.components.paintpreview.player; import android.graphics.Rect; import android.support.test.InstrumentationRegistry; -import android.support.test.filters.MediumTest; import android.support.test.uiautomator.UiDevice; import android.view.View; import android.view.ViewGroup; +import androidx.test.filters.MediumTest; + +import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -22,6 +24,7 @@ import org.chromium.base.test.util.CallbackHelper; import org.chromium.base.test.util.ScalableTimeout; import org.chromium.base.test.util.UrlUtils; import org.chromium.content_public.browser.UiThreadTaskTraits; +import org.chromium.content_public.browser.test.util.Criteria; import org.chromium.content_public.browser.test.util.CriteriaHelper; import org.chromium.ui.test.util.DummyUiActivityTestCase; import org.chromium.url.GURL; @@ -51,6 +54,7 @@ public class PaintPreviewPlayerTest extends DummyUiActivityTestCase { private PlayerManager mPlayerManager; private TestLinkClickHandler mLinkClickHandler; + private CallbackHelper mRefreshedCallback; /** * LinkClickHandler implementation for caching the last URL that was clicked. @@ -86,17 +90,14 @@ public class PaintPreviewPlayerTest extends DummyUiActivityTestCase { final View activityContentView = getActivity().findViewById(android.R.id.content); // Assert that the player view has the same dimensions as the content view. - CriteriaHelper.pollUiThread( - () -> { - boolean contentSizeNonZero = activityContentView.getWidth() > 0 - && activityContentView.getHeight() > 0; - boolean viewSizeMatchContent = - activityContentView.getWidth() == playerHostView.getWidth() - && activityContentView.getHeight() == playerHostView.getHeight(); - return contentSizeNonZero && viewSizeMatchContent; - }, - "Player size doesn't match R.id.content", TIMEOUT_MS, - CriteriaHelper.DEFAULT_POLLING_INTERVAL); + CriteriaHelper.pollUiThread(() -> { + Criteria.checkThat(activityContentView.getWidth(), Matchers.greaterThan(0)); + Criteria.checkThat(activityContentView.getHeight(), Matchers.greaterThan(0)); + Criteria.checkThat( + activityContentView.getWidth(), Matchers.is(playerHostView.getWidth())); + Criteria.checkThat( + activityContentView.getHeight(), Matchers.is(playerHostView.getHeight())); + }, TIMEOUT_MS, CriteriaHelper.DEFAULT_POLLING_INTERVAL); } /** @@ -126,23 +127,70 @@ public class PaintPreviewPlayerTest extends DummyUiActivityTestCase { assertLinkUrl(playerHostView, 422, 4965, TEST_OUT_OF_VIEWPORT_LINK_URL); } - /** - * Scrolls to the bottom fo the paint preview. - */ - private void scrollToBottom() { + @Test + @MediumTest + public void overscrollRefreshTest() throws Exception { + initPlayerManager(); UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); int deviceHeight = uiDevice.getDisplayHeight(); + int statusBarHeight = statusBarHeight(); + int navigationBarHeight = navigationBarHeight(); + int padding = 20; + int toY = deviceHeight - navigationBarHeight - padding; + int fromY = statusBarHeight + padding; + uiDevice.swipe(50, fromY, 50, toY, 5); + mRefreshedCallback.waitForFirst(); + } + + /** + * Tests that an initialization failure is reported properly. + */ + @Test + @MediumTest + public void initializationCallbackErrorReported() throws Exception { + CallbackHelper compositorErrorCallback = new CallbackHelper(); + mLinkClickHandler = new TestLinkClickHandler(); + PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> { + PaintPreviewTestService service = + new PaintPreviewTestService(UrlUtils.getIsolatedTestFilePath(TEST_DATA_DIR)); + // Use the wrong URL to simulate a failure. + mPlayerManager = new PlayerManager(new GURL("about:blank"), getActivity(), service, + TEST_DIRECTORY_KEY, mLinkClickHandler, + () -> { Assert.fail("Unexpected overscroll refresh attempted."); }, + () -> { + Assert.fail("View Ready callback occurred, but expected a failure."); + }, + 0xffffffff, () -> { compositorErrorCallback.notifyCalled(); }, false); + }); + compositorErrorCallback.waitForFirst(); + } + + private int statusBarHeight() { Rect visibleContentRect = new Rect(); getActivity().getWindow().getDecorView().getWindowVisibleDisplayFrame(visibleContentRect); - int statusBarHeight = visibleContentRect.top; + return visibleContentRect.top; + } + private int navigationBarHeight() { int navigationBarHeight = 100; int resourceId = getActivity().getResources().getIdentifier( "navigation_bar_height", "dimen", "android"); if (resourceId > 0) { navigationBarHeight = getActivity().getResources().getDimensionPixelSize(resourceId); } + return navigationBarHeight; + } + + /** + * Scrolls to the bottom fo the paint preview. + */ + private void scrollToBottom() { + UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + int deviceHeight = uiDevice.getDisplayHeight(); + + int statusBarHeight = statusBarHeight(); + int navigationBarHeight = navigationBarHeight(); int padding = 20; int swipeSteps = 5; @@ -160,24 +208,38 @@ public class PaintPreviewPlayerTest extends DummyUiActivityTestCase { private void initPlayerManager() { mLinkClickHandler = new TestLinkClickHandler(); + mRefreshedCallback = new CallbackHelper(); + CallbackHelper viewReady = new CallbackHelper(); PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> { PaintPreviewTestService service = new PaintPreviewTestService(UrlUtils.getIsolatedTestFilePath(TEST_DATA_DIR)); mPlayerManager = new PlayerManager(new GURL(TEST_URL), getActivity(), service, - TEST_DIRECTORY_KEY, mLinkClickHandler, Assert::assertTrue, 0xffffffff); + TEST_DIRECTORY_KEY, mLinkClickHandler, + () -> { mRefreshedCallback.notifyCalled(); }, + () -> { viewReady.notifyCalled(); }, + 0xffffffff, () -> { Assert.fail("Compositor initialization failed."); }, + false); getActivity().setContentView(mPlayerManager.getView()); }); // Wait until PlayerManager is initialized. - CriteriaHelper.pollUiThread(() -> mPlayerManager != null, - "PlayerManager was not initialized.", TIMEOUT_MS, - CriteriaHelper.DEFAULT_POLLING_INTERVAL); + CriteriaHelper.pollUiThread(() -> { + Criteria.checkThat( + "PlayerManager was not initialized.", mPlayerManager, Matchers.notNullValue()); + }, TIMEOUT_MS, CriteriaHelper.DEFAULT_POLLING_INTERVAL); + + try { + viewReady.waitForFirst(); + } catch (Exception e) { + Assert.fail("View ready was not called."); + } // Assert that the player view is added to the player host view. - CriteriaHelper.pollUiThread( - () -> ((ViewGroup) mPlayerManager.getView()).getChildCount() > 0, - "Player view is not added to the host view.", TIMEOUT_MS, - CriteriaHelper.DEFAULT_POLLING_INTERVAL); + CriteriaHelper.pollUiThread(() -> { + Criteria.checkThat("Player view is not added to the host view.", + ((ViewGroup) mPlayerManager.getView()).getChildCount(), + Matchers.greaterThan(0)); + }, TIMEOUT_MS, CriteriaHelper.DEFAULT_POLLING_INTERVAL); } /* @@ -209,18 +271,11 @@ public class PaintPreviewPlayerTest extends DummyUiActivityTestCase { UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); device.click(scaledX + locationXY[0], scaledY + locationXY[1]); - CriteriaHelper.pollUiThread( - () - -> { - GURL url = mLinkClickHandler.mUrl; - if (url == null) return false; - - return url.getSpec().equals(expectedUrl); - }, - "Link press on abs (" + x + ", " + y + ") failed. Expected: " + expectedUrl - + ", found: " - + (mLinkClickHandler.mUrl == null ? null - : mLinkClickHandler.mUrl.getSpec()), - TIMEOUT_MS, CriteriaHelper.DEFAULT_POLLING_INTERVAL); + CriteriaHelper.pollUiThread(() -> { + GURL url = mLinkClickHandler.mUrl; + String msg = "Link press on abs (" + x + ", " + y + ") failed."; + Criteria.checkThat(msg, url, Matchers.notNullValue()); + Criteria.checkThat(msg, url.getSpec(), Matchers.is(expectedUrl)); + }, TIMEOUT_MS, CriteriaHelper.DEFAULT_POLLING_INTERVAL); } } diff --git a/chromium/components/paint_preview/player/android/javatests/src/org/chromium/components/paintpreview/player/PaintPreviewTestRule.java b/chromium/components/paint_preview/player/android/javatests/src/org/chromium/components/paintpreview/player/PaintPreviewTestRule.java index 422d38d76e1..fb583dbae53 100644 --- a/chromium/components/paint_preview/player/android/javatests/src/org/chromium/components/paintpreview/player/PaintPreviewTestRule.java +++ b/chromium/components/paint_preview/player/android/javatests/src/org/chromium/components/paintpreview/player/PaintPreviewTestRule.java @@ -4,43 +4,39 @@ package org.chromium.components.paintpreview.player; +import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; -import org.chromium.components.signin.AccountManagerFacadeImpl; import org.chromium.components.signin.AccountManagerFacadeProvider; -import org.chromium.components.signin.test.util.FakeAccountManagerDelegate; -import org.chromium.content_public.browser.test.NativeLibraryTestRule; -import org.chromium.content_public.browser.test.util.TestThreadUtils; +import org.chromium.components.signin.test.util.FakeAccountManagerFacade; +import org.chromium.content_public.browser.test.NativeLibraryTestUtils; /** * Loads native and initializes the browser process for Paint Preview instrumentation tests. */ -public class PaintPreviewTestRule extends NativeLibraryTestRule { - private FakeAccountManagerDelegate mAccountManager; - +public class PaintPreviewTestRule implements TestRule { /** * {@link AccountManagerFacadeProvider#getInstance()} is called in the browser initialization * path. If we don't mock {@link AccountManagerFacade}, we'll run into a failed assertion. */ private void setUp() { - mAccountManager = new FakeAccountManagerDelegate( - FakeAccountManagerDelegate.DISABLE_PROFILE_DATA_SOURCE); - TestThreadUtils.runOnUiThreadBlocking(() -> { - AccountManagerFacadeProvider.setInstanceForTests( - new AccountManagerFacadeImpl(mAccountManager)); - }); - loadNativeLibraryAndInitBrowserProcess(); + AccountManagerFacadeProvider.setInstanceForTests(new FakeAccountManagerFacade(null)); + NativeLibraryTestUtils.loadNativeLibraryAndInitBrowserProcess(); } @Override public Statement apply(final Statement base, Description description) { - return super.apply(new Statement() { + return new Statement() { @Override public void evaluate() throws Throwable { setUp(); - base.evaluate(); + try { + base.evaluate(); + } finally { + AccountManagerFacadeProvider.resetInstanceForTests(); + } } - }, description); + }; } } diff --git a/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/PlayerManagerTest.java b/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/PlayerManagerTest.java index ba583b83118..b7a2c0205da 100644 --- a/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/PlayerManagerTest.java +++ b/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/PlayerManagerTest.java @@ -36,9 +36,10 @@ public class PlayerManagerTest { public void testFrameHierarchyBuilderNoSubFrames() { UnguessableToken token = makeToken(3728193L, 3283974L); PaintPreviewFrame generatedFrame1 = PlayerManager.buildFrameTreeHierarchy(token, - new UnguessableToken[] {token}, new int[] {500, 600}, new int[] {0}, null, null); + new UnguessableToken[] {token}, new int[] {500, 600}, new int[] {100, 200}, + new int[] {0}, null, null, false); PaintPreviewFrame expectedFrame1 = PaintPreviewFrame.createInstanceForTest( - token, 500, 600, new PaintPreviewFrame[] {}, new Rect[] {}); + token, 500, 600, 100, 200, new PaintPreviewFrame[] {}, new Rect[] {}); Assert.assertEquals(expectedFrame1, generatedFrame1); } @@ -52,12 +53,12 @@ public class PlayerManagerTest { UnguessableToken subframeToken = makeToken(123982L, 637846L); PaintPreviewFrame generatedFrame2 = PlayerManager.buildFrameTreeHierarchy(mainToken, new UnguessableToken[] {mainToken, subframeToken}, new int[] {500, 600, 100, 200}, - new int[] {1, 0}, new UnguessableToken[] {subframeToken}, - new int[] {10, 10, 50, 50}); + new int[] {50, 60, 0, 5}, new int[] {1, 0}, new UnguessableToken[] {subframeToken}, + new int[] {10, 10, 50, 50}, false); PaintPreviewFrame expectedFrame2Subframe = PaintPreviewFrame.createInstanceForTest( - subframeToken, 100, 200, new PaintPreviewFrame[] {}, new Rect[] {}); + subframeToken, 100, 200, 0, 5, new PaintPreviewFrame[] {}, new Rect[] {}); PaintPreviewFrame expectedFrame2 = PaintPreviewFrame.createInstanceForTest(mainToken, 500, - 600, new PaintPreviewFrame[] {expectedFrame2Subframe}, + 600, 50, 60, new PaintPreviewFrame[] {expectedFrame2Subframe}, new Rect[] {new Rect(10, 10, 60, 60)}); Assert.assertEquals(expectedFrame2, generatedFrame2); } @@ -75,17 +76,18 @@ public class PlayerManagerTest { UnguessableToken subframe2Token = makeToken(989272L, 3789489L); PaintPreviewFrame generatedFrame3 = PlayerManager.buildFrameTreeHierarchy(mainToken, new UnguessableToken[] {mainToken, subframe1Token, subframe2Token}, - new int[] {500, 600, 200, 100, 10, 20}, new int[] {1, 2, 0}, + new int[] {500, 600, 200, 100, 10, 20}, new int[] {50, 60, 20, 10, 1, 2}, + new int[] {1, 2, 0}, new UnguessableToken[] {subframe1Token, subframe2Token, subframe2Token}, - new int[] {50, 60, 100, 150, 10, 15, 20, 25, 30, 35, 5, 15}); + new int[] {50, 60, 100, 150, 10, 15, 20, 25, 30, 35, 5, 15}, false); PaintPreviewFrame expectedFrame3Subframe2 = PaintPreviewFrame.createInstanceForTest( - subframe2Token, 10, 20, new PaintPreviewFrame[] {}, new Rect[] {}); + subframe2Token, 10, 20, 1, 2, new PaintPreviewFrame[] {}, new Rect[] {}); PaintPreviewFrame expectedFrame3Subframe1 = - PaintPreviewFrame.createInstanceForTest(subframe1Token, 200, 100, + PaintPreviewFrame.createInstanceForTest(subframe1Token, 200, 100, 20, 10, new PaintPreviewFrame[] {expectedFrame3Subframe2, expectedFrame3Subframe2}, new Rect[] {new Rect(10, 15, 30, 40), new Rect(30, 35, 35, 50)}); PaintPreviewFrame expectedFrame3 = PaintPreviewFrame.createInstanceForTest(mainToken, 500, - 600, new PaintPreviewFrame[] {expectedFrame3Subframe1}, + 600, 50, 60, new PaintPreviewFrame[] {expectedFrame3Subframe1}, new Rect[] {new Rect(50, 60, 150, 210)}); Assert.assertEquals(expectedFrame3, generatedFrame3); } diff --git a/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/frame/PaintPreviewCustomFlingingShadowScroller.java b/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/frame/PaintPreviewCustomFlingingShadowScroller.java index 26a2eecaae1..2a2525d25c3 100644 --- a/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/frame/PaintPreviewCustomFlingingShadowScroller.java +++ b/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/frame/PaintPreviewCustomFlingingShadowScroller.java @@ -5,7 +5,7 @@ package org.chromium.components.paintpreview.player.frame; import android.content.Context; -import android.widget.Scroller; +import android.widget.OverScroller; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -13,7 +13,7 @@ import org.robolectric.annotation.Implements; /** * A custom shadow of {@link Scroller} that supports fake flinging. */ -@Implements(Scroller.class) +@Implements(OverScroller.class) public class PaintPreviewCustomFlingingShadowScroller { private int mFinalX; private int mFinalY; diff --git a/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediatorTest.java b/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediatorTest.java index f9badc92c0f..ec49bee8b2e 100644 --- a/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediatorTest.java +++ b/chromium/components/paint_preview/player/android/junit/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediatorTest.java @@ -4,12 +4,17 @@ package org.chromium.components.paintpreview.player.frame; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; + import android.graphics.Bitmap; +import android.graphics.Matrix; import android.graphics.Rect; import android.os.Parcel; import android.util.Pair; import android.view.View; -import android.widget.Scroller; +import android.widget.OverScroller; import androidx.annotation.NonNull; @@ -17,6 +22,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; import org.mockito.Mockito; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLooper; @@ -44,7 +50,7 @@ public class PlayerFrameMediatorTest { private UnguessableToken mFrameGuid; private PropertyModel mModel; private TestPlayerCompositorDelegate mCompositorDelegate; - private Scroller mScroller; + private OverScroller mScroller; private PlayerFrameMediator mMediator; /** @@ -160,14 +166,27 @@ public class PlayerFrameMediatorTest { } } + private class MatrixMatcher implements ArgumentMatcher<Matrix> { + private Matrix mLeft; + + MatrixMatcher(Matrix left) { + mLeft = left; + } + + @Override + public boolean matches(Matrix right) { + return mLeft.equals(right); + } + } + @Before public void setUp() { mFrameGuid = frameGuid(); mModel = new PropertyModel.Builder(PlayerFrameProperties.ALL_KEYS).build(); mCompositorDelegate = new TestPlayerCompositorDelegate(); - mScroller = new Scroller(ContextUtils.getApplicationContext()); - mMediator = new PlayerFrameMediator( - mModel, mCompositorDelegate, mScroller, mFrameGuid, CONTENT_WIDTH, CONTENT_HEIGHT); + mScroller = new OverScroller(ContextUtils.getApplicationContext()); + mMediator = new PlayerFrameMediator(mModel, mCompositorDelegate, mScroller, mFrameGuid, + CONTENT_WIDTH, CONTENT_HEIGHT, 0, 0); } private static Rect getRectForTile(int tileWidth, int tileHeight, int row, int col) { @@ -706,8 +725,561 @@ public class PlayerFrameMediatorTest { } /** - * TODO(crbug.com/1020702): Implement after zooming support is added. + * Tests that {@link PlayerFrameMediator} correctly consumes scale events and scales between + * the allowed range. + */ + @Test + public void testViewPortOnScaleBy() { + // Initial view port setup. + mMediator.setLayoutDimensions(100, 200); + mMediator.updateViewportSize(100, 200, 1f); + + // The current view port fully matches the top left bitmap tile. + // Below is a schematic of the entire bitmap matrix. Tiles marked with x are required for + // the current view port. + // ------------------------- + // | x | x | | | | | + // ------------------------- + // | x | | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + boolean[][] expectedRequiredBitmaps = new boolean[6][6]; + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(1f))); + + // Now a scale factor of 2 will be applied. This will happen at a focal point of 0, 0. + // The same bitmaps will be required but the grid will be double the size. + Assert.assertTrue(mMediator.scaleBy(2f, 0, 0)); + Assert.assertTrue(mMediator.scaleFinished(1f, 0, 0)); + + expectedRequiredBitmaps = new boolean[12][12]; + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(2f))); + Assert.assertNull(mMediator.mRequiredBitmaps.get(1f)); + + // Reduce the scale factor by 0.5 returning to a scale of 1. + Assert.assertTrue(mMediator.scaleBy(0.5f, 0, 0)); + Assert.assertTrue(mMediator.scaleFinished(1f, 0, 0)); + + expectedRequiredBitmaps = new boolean[6][6]; + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(1f))); + Assert.assertNull(mMediator.mRequiredBitmaps.get(2f)); + + // Increase the scale factor to 6 which is above the maximum limit returning to a scale + // of 5. Note that the grid is smaller than 30x30 as the viewport is not a multiple of the + // content width and height so there is difference. + Assert.assertTrue(mMediator.scaleBy(6f, 0, 0)); + Assert.assertTrue(mMediator.scaleFinished(1f, 0, 0)); + + expectedRequiredBitmaps = new boolean[29][28]; + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(5f))); + Assert.assertNull(mMediator.mRequiredBitmaps.get(1f)); + + // Reduce the scale factor back to 1. + Assert.assertTrue(mMediator.scaleBy(0.2f, 0, 0)); + Assert.assertTrue(mMediator.scaleFinished(1f, 0, 0)); + + expectedRequiredBitmaps = new boolean[6][6]; + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(1f))); + Assert.assertNull(mMediator.mRequiredBitmaps.get(5f)); + + // We now reduce the scale factor to less than mInitialScaleFactor; however, the maximum + // scale out is limited to mInitialScaleFactor. + float initialScaleFactor = 100f / 560f; + Assert.assertTrue(mMediator.scaleBy(initialScaleFactor, 0, 0)); + Assert.assertTrue(mMediator.scaleBy(0.5f, 0, 0)); + Assert.assertTrue(mMediator.scaleFinished(1f, 0, 0)); + + expectedRequiredBitmaps = new boolean[2][1]; + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[1][0] = true; + Assert.assertTrue(Arrays.deepEquals( + expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(initialScaleFactor))); + Assert.assertNull(mMediator.mRequiredBitmaps.get(1f)); + } + + /** + * Tests that {@link PlayerFrameMediator} works correctly when scrolling. + */ + @Test + public void testViewPortOnScaleByWithScroll() { + // Initial view port setup. + mMediator.updateViewportSize(100, 200, 1f); + + boolean[][] expectedRequiredBitmaps = new boolean[6][6]; + + // STEP 1: Original request. + // The current view port fully matches the top left bitmap tile. + // Below is a schematic of the entire bitmap matrix. Tiles marked with x are required for + // the current view port. + // ------------------------- + // | x | x | | | | | + // ------------------------- + // | x | | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + List<RequestedBitmap> expectedRequestedBitmaps = new ArrayList<>(); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 0), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 0), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 1), 1f)); + + // Both matricies should be identity to start. + Assert.assertTrue(mMediator.mViewportScaleMatrix.isIdentity()); + Assert.assertTrue(mModel.get(PlayerFrameProperties.SCALE_MATRIX).isIdentity()); + // Ensure the correct bitmaps are required and requested. + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(1f))); + Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap); + + // STEP 2: Scroll slightly. + mMediator.scrollBy(10, 15); + // The current viewport covers portions of the 4 top left bitmap tiles. + // ------------------------- + // | x | x | x | | | | + // ------------------------- + // | x | x | x | | | | + // ------------------------- + // | x | x | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + expectedRequiredBitmaps[0][2] = true; + expectedRequiredBitmaps[1][1] = true; + expectedRequiredBitmaps[1][2] = true; + expectedRequiredBitmaps[2][0] = true; + expectedRequiredBitmaps[2][1] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(1f))); + + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 1), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 2, 0), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 2), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 2, 1), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 2), 1f)); + Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap); + + // The viewport matrix should track scroll and zoom. + Matrix expectedViewportMatrix = new Matrix(); + float[] expectedViewportMatrixValues = new float[9]; + expectedViewportMatrix.getValues(expectedViewportMatrixValues); + expectedViewportMatrixValues[Matrix.MTRANS_X] = -10; + expectedViewportMatrixValues[Matrix.MTRANS_Y] = -15; + expectedViewportMatrix.setValues(expectedViewportMatrixValues); + + Assert.assertEquals(expectedViewportMatrix, mMediator.mViewportScaleMatrix); + Assert.assertTrue(mModel.get(PlayerFrameProperties.SCALE_MATRIX).isIdentity()); + + // STEP 3: Now a scale factor of 2 will be applied. This will happen at a focal point of 50, + // 100. + Assert.assertTrue(mMediator.scaleBy(2f, 50f, 100f)); + + // Before the scaling commits both matricies should update. + expectedViewportMatrix.postScale(2f, 2f, 50f, 100f); + Matrix expectedBitmapMatrix = new Matrix(); + expectedBitmapMatrix.postScale(2f, 2f, 50f, 100f); + Assert.assertEquals(expectedViewportMatrix, mMediator.mViewportScaleMatrix); + Assert.assertEquals(expectedBitmapMatrix, mModel.get(PlayerFrameProperties.SCALE_MATRIX)); + + // Bitmaps should be the same as before scaling until scaling is finished. + Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap); + mCompositorDelegate.mRequestedBitmap.clear(); + expectedRequestedBitmaps.clear(); + + Assert.assertTrue(mMediator.scaleFinished(1f, 0, 0)); + + expectedRequiredBitmaps = new boolean[12][12]; + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + expectedRequiredBitmaps[0][2] = true; + expectedRequiredBitmaps[1][1] = true; + expectedRequiredBitmaps[1][2] = true; + expectedRequiredBitmaps[2][0] = true; + expectedRequiredBitmaps[2][1] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(2f))); + Assert.assertNull(mMediator.mRequiredBitmaps.get(1f)); + + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 0), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 0), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 1), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 1), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 2, 0), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 2), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 2, 1), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 2), 2f)); + Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap); + + // The bitmap matrix should be cleared. + expectedBitmapMatrix.reset(); + Assert.assertTrue(mModel.get(PlayerFrameProperties.SCALE_MATRIX).isIdentity()); + + // STEP4: Now a scale factor of 0.5 will be applied. This will happen at a focal point of + // 50, 100. + Assert.assertTrue(mMediator.scaleBy(0.5f, 50f, 100f)); + + // Ensure the matricies are correct mid-scale. + expectedViewportMatrix.postScale(0.5f, 0.5f, 50f, 100f); + expectedBitmapMatrix.postScale(0.5f, 0.5f, 50f, 100f); + Assert.assertEquals(expectedViewportMatrix, mMediator.mViewportScaleMatrix); + Assert.assertEquals(expectedBitmapMatrix, mModel.get(PlayerFrameProperties.SCALE_MATRIX)); + + // Bitmaps should be the same as before scaling until scaling is finished. + Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap); + mCompositorDelegate.mRequestedBitmap.clear(); + expectedRequestedBitmaps.clear(); + + Assert.assertTrue(mMediator.scaleFinished(1f, 0, 0)); + + expectedRequiredBitmaps = new boolean[6][6]; + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + expectedRequiredBitmaps[0][2] = true; + expectedRequiredBitmaps[1][1] = true; + expectedRequiredBitmaps[1][2] = true; + expectedRequiredBitmaps[2][0] = true; + expectedRequiredBitmaps[2][1] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(1f))); + Assert.assertNull(mMediator.mRequiredBitmaps.get(2f)); + + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 0), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 0), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 1), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 1), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 2, 0), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 2), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 2, 1), 1f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 2), 1f)); + Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap); + + expectedBitmapMatrix.reset(); + Assert.assertTrue(mModel.get(PlayerFrameProperties.SCALE_MATRIX).isIdentity()); + + // Now a scale factor of 2 will be applied. This will happen at a focal point of 100, 200. + // Due to the position of the focal point the required bitmaps will move. + // ------------------------- + // | | x | x | | | | + // ------------------------- + // | x | x | x | x | | | + // ------------------------- + // | x | x | x | x | | | + // ------------------------- + // | | x | x | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + Assert.assertTrue(mMediator.scaleBy(2f, 100f, 200f)); + + expectedViewportMatrix.postScale(2f, 2f, 100f, 200f); + expectedBitmapMatrix.postScale(2f, 2f, 100f, 200f); + Assert.assertEquals(expectedViewportMatrix, mMediator.mViewportScaleMatrix); + Assert.assertEquals(expectedBitmapMatrix, mModel.get(PlayerFrameProperties.SCALE_MATRIX)); + + Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap); + mCompositorDelegate.mRequestedBitmap.clear(); + expectedRequestedBitmaps.clear(); + + Assert.assertTrue(mMediator.scaleFinished(1f, 0, 0)); + + expectedRequiredBitmaps = new boolean[12][12]; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[0][2] = true; + expectedRequiredBitmaps[1][0] = true; + expectedRequiredBitmaps[1][1] = true; + expectedRequiredBitmaps[1][2] = true; + expectedRequiredBitmaps[1][3] = true; + expectedRequiredBitmaps[2][0] = true; + expectedRequiredBitmaps[2][1] = true; + expectedRequiredBitmaps[2][2] = true; + expectedRequiredBitmaps[2][3] = true; + expectedRequiredBitmaps[3][1] = true; + expectedRequiredBitmaps[3][2] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(2f))); + Assert.assertNull(mMediator.mRequiredBitmaps.get(1f)); + + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 1), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 2, 1), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 2), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 2, 2), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 1), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 0), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 3, 1), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 2, 0), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 0, 2), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 1, 3), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 3, 2), 2f)); + expectedRequestedBitmaps.add( + new RequestedBitmap(mFrameGuid, getRectForTile(100, 200, 2, 3), 2f)); + Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap); + + expectedBitmapMatrix.reset(); + Assert.assertTrue(mModel.get(PlayerFrameProperties.SCALE_MATRIX).isIdentity()); + } + + /** + * Tests that {@link PlayerFrameMediator} works correctly when subframes are present. */ @Test - public void testViewPortOnScaleBy() {} + public void testViewPortOnScaleByWithSubFrames() { + PlayerFrameView subFrame1View = Mockito.mock(PlayerFrameView.class); + Pair<View, Rect> subFrame1 = new Pair<>(subFrame1View, new Rect(10, 20, 60, 40)); + PlayerFrameView subFrame2View = Mockito.mock(PlayerFrameView.class); + Pair<View, Rect> subFrame2 = new Pair<>(subFrame2View, new Rect(30, 50, 70, 160)); + + mMediator.addSubFrame(subFrame1.first, subFrame1.second); + mMediator.addSubFrame(subFrame2.first, subFrame2.second); + + // Both subframes should be visible. + mMediator.updateViewportSize(50, 100, 1f); + List<View> expectedVisibleViews = new ArrayList<>(); + List<Rect> expectedVisibleRects = new ArrayList<>(); + expectedVisibleViews.add(subFrame1.first); + expectedVisibleViews.add(subFrame2.first); + expectedVisibleRects.add(subFrame1.second); + expectedVisibleRects.add(subFrame2.second); + Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS)); + Assert.assertEquals(expectedVisibleRects, mModel.get(PlayerFrameProperties.SUBFRAME_RECTS)); + + expectedVisibleViews.clear(); + expectedVisibleRects.clear(); + expectedVisibleViews.add(subFrame1.first); + expectedVisibleRects.add(new Rect(20, 40, 120, 80)); + + // During scaling the second subframe should disappear from the viewport. + Assert.assertTrue(mMediator.scaleBy(2f, 0f, 0f)); + Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS)); + Assert.assertEquals(expectedVisibleRects, mModel.get(PlayerFrameProperties.SUBFRAME_RECTS)); + Matrix expectedMatrix = new Matrix(); + expectedMatrix.setScale(2f, 2f); + verify(subFrame1View) + .updateDelegateScaleMatrix(argThat(new MatrixMatcher(expectedMatrix)), eq(2f)); + + Assert.assertTrue(mMediator.scaleFinished(1f, 0f, 0f)); + Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS)); + Assert.assertEquals(expectedVisibleRects, mModel.get(PlayerFrameProperties.SUBFRAME_RECTS)); + expectedMatrix.reset(); + verify(subFrame1View) + .updateDelegateScaleMatrix(argThat(new MatrixMatcher(expectedMatrix)), eq(1f)); + verify(subFrame1View).forceRedraw(); + + // Scroll so the second subframe is back in the viewport.. + mMediator.scrollBy(20, 40); + expectedVisibleViews.add(subFrame2.first); + expectedVisibleRects.clear(); + expectedVisibleRects.add(new Rect(0, 0, 100, 40)); + expectedVisibleRects.add(new Rect(40, 60, 120, 280)); + Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS)); + Assert.assertEquals(expectedVisibleRects, mModel.get(PlayerFrameProperties.SUBFRAME_RECTS)); + + // Scale out keeping the subframes in the viewport.. + Assert.assertTrue(mMediator.scaleBy(0.75f, 25f, 50f)); + expectedVisibleRects.clear(); + expectedVisibleRects.add(new Rect(6, 13, 81, 43)); + expectedVisibleRects.add(new Rect(36, 58, 96, 223)); + Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS)); + Assert.assertEquals(expectedVisibleRects, mModel.get(PlayerFrameProperties.SUBFRAME_RECTS)); + expectedMatrix.setScale(0.75f, 0.75f); + verify(subFrame1View) + .updateDelegateScaleMatrix(argThat(new MatrixMatcher(expectedMatrix)), eq(1.5f)); + verify(subFrame2View) + .updateDelegateScaleMatrix(argThat(new MatrixMatcher(expectedMatrix)), eq(1.5f)); + } + + /** + * Tests that {@link PlayerFrameMediator} correctly scales and keeps the content in bounds. + */ + @Test + public void testViewPortOnScaleByWithinBounds() { + // Initial view port setup. + mMediator.updateViewportSize(100, 200, 1f); + + boolean[][] expectedRequiredBitmaps = new boolean[6][6]; + + // The current view port fully matches the top left bitmap tile. + // Below is a schematic of the entire bitmap matrix. Tiles marked with x are required for + // the current view port. + // ------------------------- + // | x | x | | | | | + // ------------------------- + // | x | | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + // ------------------------- + // | | | | | | | + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(1f))); + + // Now a scale factor of 2 will be applied. This will happen at a focal point of 0, 0. + // The same bitmaps will be required but the grid will be double the size. + Assert.assertTrue(mMediator.scaleBy(2f, 0, 0)); + Matrix expectedViewportMatrix = new Matrix(); + Matrix expectedBitmapMatrix = new Matrix(); + expectedViewportMatrix.postScale(2f, 2f, 0f, 0f); + expectedBitmapMatrix.postScale(2f, 2f, 0f, 0f); + Assert.assertEquals(expectedViewportMatrix, mMediator.mViewportScaleMatrix); + Assert.assertEquals(expectedBitmapMatrix, mModel.get(PlayerFrameProperties.SCALE_MATRIX)); + + Assert.assertTrue(mMediator.scaleFinished(1f, 0, 0)); + + expectedRequiredBitmaps = new boolean[12][12]; + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(2f))); + Assert.assertNull(mMediator.mRequiredBitmaps.get(1f)); + + // Reduce the scale factor by 0.5 returning to a scale of 1 but try to do so with a focal + // point that causes translation outside the bounds. The focal point should be ignored. + Assert.assertTrue(mMediator.scaleBy(0.5f, 50f, 50f)); + expectedViewportMatrix.postScale(0.5f, 0.5f, 0f, 0f); + expectedBitmapMatrix.reset(); + expectedBitmapMatrix.postScale(0.5f, 0.5f, 0f, 0f); + Assert.assertEquals(expectedViewportMatrix, mMediator.mViewportScaleMatrix); + Assert.assertEquals(expectedBitmapMatrix, mModel.get(PlayerFrameProperties.SCALE_MATRIX)); + + Assert.assertTrue(mMediator.scaleFinished(1f, -50f, -50f)); + + expectedRequiredBitmaps = new boolean[6][6]; + expectedRequiredBitmaps[0][0] = true; + expectedRequiredBitmaps[0][1] = true; + expectedRequiredBitmaps[1][0] = true; + Assert.assertTrue( + Arrays.deepEquals(expectedRequiredBitmaps, mMediator.mRequiredBitmaps.get(1f))); + Assert.assertNull(mMediator.mRequiredBitmaps.get(2f)); + } + + /** + * Tests that {@link PlayerFrameMediator} works correctly with nested subframes. This test + * pretends that mMediator is for a subframe. The calls made to this mediator are verified + * to occur in {@link testViewPortOnScaleByWithSubFrames}. + */ + @Test + public void testViewPortOnScaleByWithNestedSubFrames() { + PlayerFrameView subFrameView = Mockito.mock(PlayerFrameView.class); + Pair<View, Rect> subFrame = new Pair<>(subFrameView, new Rect(10, 20, 60, 40)); + mMediator.addSubFrame(subFrame.first, subFrame.second); + + // The subframe should be visible. + mMediator.updateViewportSize(50, 100, 1f); + List<View> expectedVisibleViews = new ArrayList<>(); + List<Rect> expectedVisibleRects = new ArrayList<>(); + expectedVisibleViews.add(subFrame.first); + expectedVisibleRects.add(subFrame.second); + Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS)); + Assert.assertEquals(expectedVisibleRects, mModel.get(PlayerFrameProperties.SUBFRAME_RECTS)); + + expectedVisibleViews.clear(); + expectedVisibleRects.clear(); + expectedVisibleViews.add(subFrame.first); + expectedVisibleRects.add(new Rect(20, 40, 120, 80)); + + // Scale by a factor of two via the parent. + Matrix scaleMatrix = new Matrix(); + scaleMatrix.setScale(2f, 2f); + mMediator.setBitmapScaleMatrix(scaleMatrix, 2f); + Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS)); + Assert.assertEquals(expectedVisibleRects, mModel.get(PlayerFrameProperties.SUBFRAME_RECTS)); + verify(subFrameView) + .updateDelegateScaleMatrix(argThat(new MatrixMatcher(scaleMatrix)), eq(2f)); + + expectedVisibleViews.clear(); + expectedVisibleRects.clear(); + expectedVisibleViews.add(subFrame.first); + expectedVisibleRects.add(new Rect(15, 30, 90, 60)); + + // Zoom out by a factor of 0.75, note the scale factor argument is compounded. + scaleMatrix.setScale(0.75f, 0.75f); + mMediator.setBitmapScaleMatrix(scaleMatrix, 1.5f); + Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS)); + Assert.assertEquals(expectedVisibleRects, mModel.get(PlayerFrameProperties.SUBFRAME_RECTS)); + verify(subFrameView) + .updateDelegateScaleMatrix(argThat(new MatrixMatcher(scaleMatrix)), eq(1.5f)); + + // Force a redraw and ensure it is recursive. + mMediator.forceRedraw(); + verify(subFrameView).forceRedraw(); + } } diff --git a/chromium/components/paint_preview/player/android/player_compositor_delegate_android.cc b/chromium/components/paint_preview/player/android/player_compositor_delegate_android.cc index ee2c7928dfc..ead1788baef 100644 --- a/chromium/components/paint_preview/player/android/player_compositor_delegate_android.cc +++ b/chromium/components/paint_preview/player/android/player_compositor_delegate_android.cc @@ -11,6 +11,7 @@ #include "base/android/jni_string.h" #include "base/android/unguessable_token_android.h" #include "base/bind.h" +#include "base/metrics/histogram_functions.h" #include "base/task/post_task.h" #include "base/trace_event/common/trace_event_common.h" #include "base/unguessable_token.h" @@ -49,7 +50,8 @@ ScopedJavaLocalRef<jobjectArray> ToJavaUnguessableTokenArray( } ScopedJavaGlobalRef<jobject> ConvertToJavaBitmap(const SkBitmap& sk_bitmap) { - return ScopedJavaGlobalRef<jobject>(gfx::ConvertToJavaBitmap(&sk_bitmap)); + return ScopedJavaGlobalRef<jobject>( + gfx::ConvertToJavaBitmap(&sk_bitmap, gfx::OomBehavior::kReturnNullOnOom)); } } // namespace @@ -59,12 +61,13 @@ jlong JNI_PlayerCompositorDelegateImpl_Initialize( const JavaParamRef<jobject>& j_object, jlong paint_preview_service, const JavaParamRef<jstring>& j_url_spec, - const JavaParamRef<jstring>& j_directory_key) { + const JavaParamRef<jstring>& j_directory_key, + const JavaParamRef<jobject>& j_compositor_error_callback) { PlayerCompositorDelegateAndroid* delegate = new PlayerCompositorDelegateAndroid( env, j_object, reinterpret_cast<PaintPreviewBaseService*>(paint_preview_service), - j_url_spec, j_directory_key); + j_url_spec, j_directory_key, j_compositor_error_callback); return reinterpret_cast<intptr_t>(delegate); } @@ -73,32 +76,50 @@ PlayerCompositorDelegateAndroid::PlayerCompositorDelegateAndroid( const JavaParamRef<jobject>& j_object, PaintPreviewBaseService* paint_preview_service, const JavaParamRef<jstring>& j_url_spec, - const JavaParamRef<jstring>& j_directory_key) + const JavaParamRef<jstring>& j_directory_key, + const JavaParamRef<jobject>& j_compositor_error_callback) : PlayerCompositorDelegate( paint_preview_service, GURL(base::android::ConvertJavaStringToUTF8(env, j_url_spec)), DirectoryKey{ - base::android::ConvertJavaStringToUTF8(env, j_directory_key)}), - request_id_(0) { + base::android::ConvertJavaStringToUTF8(env, j_directory_key)}, + base::BindOnce( + &base::android::RunRunnableAndroid, + ScopedJavaGlobalRef<jobject>(j_compositor_error_callback))), + request_id_(0), + startup_timestamp_(base::TimeTicks::Now()) { java_ref_.Reset(env, j_object); } void PlayerCompositorDelegateAndroid::OnCompositorReady( mojom::PaintPreviewCompositor::Status status, mojom::PaintPreviewBeginCompositeResponsePtr composite_response) { + bool compositor_started = + status == mojom::PaintPreviewCompositor::Status::kSuccess; + base::UmaHistogramBoolean( + "Browser.PaintPreview.Player.CompositorProcessStartedCorrectly", + compositor_started); + if (!compositor_started && compositor_error_) { + std::move(compositor_error_).Run(); + return; + } + base::UmaHistogramTimes( + "Browser.PaintPreview.Player.CompositorProcessStartupTime", + base::TimeTicks::Now() - startup_timestamp_); JNIEnv* env = base::android::AttachCurrentThread(); std::vector<base::UnguessableToken> all_guids; std::vector<int> scroll_extents; + std::vector<int> scroll_offsets; std::vector<int> subframe_count; std::vector<base::UnguessableToken> subframe_ids; std::vector<int> subframe_rects; base::UnguessableToken root_frame_guid; if (composite_response) { - CompositeResponseFramesToVectors(composite_response->frames, &all_guids, - &scroll_extents, &subframe_count, - &subframe_ids, &subframe_rects); + CompositeResponseFramesToVectors( + composite_response->frames, &all_guids, &scroll_extents, + &scroll_offsets, &subframe_count, &subframe_ids, &subframe_rects); root_frame_guid = composite_response->root_frame_guid; } else { // If there is no composite response due to a failure we don't have a root @@ -111,6 +132,8 @@ void PlayerCompositorDelegateAndroid::OnCompositorReady( ToJavaUnguessableTokenArray(env, all_guids); ScopedJavaLocalRef<jintArray> j_scroll_extents = base::android::ToJavaIntArray(env, scroll_extents); + ScopedJavaLocalRef<jintArray> j_scroll_offsets = + base::android::ToJavaIntArray(env, scroll_offsets); ScopedJavaLocalRef<jintArray> j_subframe_count = base::android::ToJavaIntArray(env, subframe_count); ScopedJavaLocalRef<jobjectArray> j_subframe_ids = @@ -121,11 +144,8 @@ void PlayerCompositorDelegateAndroid::OnCompositorReady( base::android::UnguessableTokenAndroid::Create(env, root_frame_guid); Java_PlayerCompositorDelegateImpl_onCompositorReady( - env, java_ref_, - static_cast<jboolean>(status == - mojom::PaintPreviewCompositor::Status::kSuccess), - j_root_frame_guid, j_all_guids, j_scroll_extents, j_subframe_count, - j_subframe_ids, j_subframe_rects); + env, java_ref_, j_root_frame_guid, j_all_guids, j_scroll_extents, + j_scroll_offsets, j_subframe_count, j_subframe_ids, j_subframe_rects); } // static @@ -133,6 +153,7 @@ void PlayerCompositorDelegateAndroid::CompositeResponseFramesToVectors( const base::flat_map<base::UnguessableToken, mojom::FrameDataPtr>& frames, std::vector<base::UnguessableToken>* all_guids, std::vector<int>* scroll_extents, + std::vector<int>* scroll_offsets, std::vector<int>* subframe_count, std::vector<base::UnguessableToken>* subframe_ids, std::vector<int>* subframe_rects) { @@ -144,6 +165,8 @@ void PlayerCompositorDelegateAndroid::CompositeResponseFramesToVectors( all_guids->push_back(pair.first); scroll_extents->push_back(pair.second->scroll_extents.width()); scroll_extents->push_back(pair.second->scroll_extents.height()); + scroll_offsets->push_back(pair.second->scroll_offsets.width()); + scroll_offsets->push_back(pair.second->scroll_offsets.height()); subframe_count->push_back(pair.second->subframes.size()); all_subframes_count += pair.second->subframes.size(); } @@ -200,15 +223,30 @@ void PlayerCompositorDelegateAndroid::OnBitmapCallback( TRACE_ID_LOCAL(request_id), "status", static_cast<int>(status), "bytes", sk_bitmap.computeByteSize()); - if (status == mojom::PaintPreviewCompositor::Status::kSuccess) { + if (status == mojom::PaintPreviewCompositor::Status::kSuccess && + !sk_bitmap.isNull()) { base::ThreadPool::PostTaskAndReplyWithResult( FROM_HERE, {base::TaskPriority::USER_VISIBLE}, base::BindOnce(&ConvertToJavaBitmap, sk_bitmap), - base::BindOnce(&base::android::RunObjectCallbackAndroid, - j_bitmap_callback)); + base::BindOnce(base::BindOnce( + [](const ScopedJavaGlobalRef<jobject>& j_bitmap_callback, + const ScopedJavaGlobalRef<jobject>& j_error_callback, + const ScopedJavaGlobalRef<jobject>& j_bitmap) { + if (!j_bitmap) { + base::android::RunRunnableAndroid(j_error_callback); + return; + } + base::android::RunObjectCallbackAndroid(j_bitmap_callback, + j_bitmap); + }, + j_bitmap_callback, j_error_callback))); } else { base::android::RunRunnableAndroid(j_error_callback); } + if (request_id == 0) { + base::UmaHistogramTimes("Browser.PaintPreview.Player.TimeToFirstBitmap", + base::TimeTicks::Now() - startup_timestamp_); + } } void PlayerCompositorDelegateAndroid::OnClick( @@ -222,6 +260,7 @@ void PlayerCompositorDelegateAndroid::OnClick( gfx::Rect(static_cast<int>(j_x), static_cast<int>(j_y), 1U, 1U)); if (res.empty()) return; + base::UmaHistogramBoolean("Browser.PaintPreview.Player.LinkClicked", true); // TODO(crbug/1061435): Resolve cases where there are multiple links. // For now just return the first in the list. Java_PlayerCompositorDelegateImpl_onLinkClicked( diff --git a/chromium/components/paint_preview/player/android/player_compositor_delegate_android.h b/chromium/components/paint_preview/player/android/player_compositor_delegate_android.h index 8c9556ef5ca..aec2b24f199 100644 --- a/chromium/components/paint_preview/player/android/player_compositor_delegate_android.h +++ b/chromium/components/paint_preview/player/android/player_compositor_delegate_android.h @@ -19,10 +19,11 @@ class PlayerCompositorDelegateAndroid : public PlayerCompositorDelegate { public: PlayerCompositorDelegateAndroid( JNIEnv* env, - const base::android::JavaParamRef<jobject>& jobject, + const base::android::JavaParamRef<jobject>& j_object, PaintPreviewBaseService* paint_preview_service, const base::android::JavaParamRef<jstring>& j_url_spec, - const base::android::JavaParamRef<jstring>& j_directory_key); + const base::android::JavaParamRef<jstring>& j_directory_key, + const base::android::JavaParamRef<jobject>& j_compositor_error_callback); void OnCompositorReady( mojom::PaintPreviewCompositor::Status status, @@ -54,6 +55,7 @@ class PlayerCompositorDelegateAndroid : public PlayerCompositorDelegate { const base::flat_map<base::UnguessableToken, mojom::FrameDataPtr>& frames, std::vector<base::UnguessableToken>* all_guids, std::vector<int>* scroll_extents, + std::vector<int>* scroll_offsets, std::vector<int>* subframe_count, std::vector<base::UnguessableToken>* subframe_ids, std::vector<int>* subframe_rects); @@ -72,6 +74,7 @@ class PlayerCompositorDelegateAndroid : public PlayerCompositorDelegate { base::android::ScopedJavaGlobalRef<jobject> java_ref_; int request_id_; + base::TimeTicks startup_timestamp_; base::WeakPtrFactory<PlayerCompositorDelegateAndroid> weak_factory_{this}; diff --git a/chromium/components/paint_preview/player/android/player_compositor_delegate_android_unittest.cc b/chromium/components/paint_preview/player/android/player_compositor_delegate_android_unittest.cc index 07328828993..193baae2b8b 100644 --- a/chromium/components/paint_preview/player/android/player_compositor_delegate_android_unittest.cc +++ b/chromium/components/paint_preview/player/android/player_compositor_delegate_android_unittest.cc @@ -32,6 +32,10 @@ TEST(PlayerCompositorDelegateAndroidTest, frame_data_subframe_1->scroll_extents = gfx::Size(50, 60); frame_data_subframe_2->scroll_extents = gfx::Size(10, 20); + frame_data_main->scroll_offsets = gfx::Size(150, 250); + frame_data_subframe_1->scroll_offsets = gfx::Size(55, 65); + frame_data_subframe_2->scroll_offsets = gfx::Size(15, 25); + mojom::SubframeClipRect clip_rect1(subframe_1_guid, gfx::Rect(5, 10, 50, 60)); mojom::SubframeClipRect clip_rect2(subframe_2_guid, gfx::Rect(15, 25, 30, 40)); @@ -44,16 +48,18 @@ TEST(PlayerCompositorDelegateAndroidTest, std::vector<base::UnguessableToken> all_guids; std::vector<int> scroll_extents; + std::vector<int> scroll_offsets; std::vector<int> subframe_count; std::vector<base::UnguessableToken> subframe_ids; std::vector<int> subframe_rects; PlayerCompositorDelegateAndroid::CompositeResponseFramesToVectors( - frames, &all_guids, &scroll_extents, &subframe_count, &subframe_ids, - &subframe_rects); + frames, &all_guids, &scroll_extents, &scroll_offsets, &subframe_count, + &subframe_ids, &subframe_rects); EXPECT_EQ(all_guids.size(), frames.size()); EXPECT_EQ(scroll_extents.size(), 2 * frames.size()); + EXPECT_EQ(scroll_offsets.size(), 2 * frames.size()); EXPECT_EQ(subframe_count.size(), frames.size()); EXPECT_EQ(subframe_ids.size(), 2U); // 2 subframes. EXPECT_EQ(subframe_rects.size(), 2U * 4U); // 2 * subframes * 4 per rect. @@ -62,11 +68,13 @@ TEST(PlayerCompositorDelegateAndroidTest, auto it = frames.find(all_guids[i]); ASSERT_NE(it, frames.end()); - const size_t scroll_offset = i * 2; - EXPECT_EQ(scroll_extents[scroll_offset], - it->second->scroll_extents.width()); - EXPECT_EQ(scroll_extents[scroll_offset + 1], + const size_t scroll_index = i * 2; + EXPECT_EQ(scroll_extents[scroll_index], it->second->scroll_extents.width()); + EXPECT_EQ(scroll_extents[scroll_index + 1], it->second->scroll_extents.height()); + EXPECT_EQ(scroll_offsets[scroll_index], it->second->scroll_offsets.width()); + EXPECT_EQ(scroll_offsets[scroll_index + 1], + it->second->scroll_offsets.height()); EXPECT_EQ(subframe_count[i], static_cast<int>(it->second->subframes.size())); for (size_t j = 0; j < it->second->subframes.size(); ++j) { diff --git a/chromium/components/paint_preview/player/player_compositor_delegate.cc b/chromium/components/paint_preview/player/player_compositor_delegate.cc index a49839c1a05..1e4e2f79391 100644 --- a/chromium/components/paint_preview/player/player_compositor_delegate.cc +++ b/chromium/components/paint_preview/player/player_compositor_delegate.cc @@ -8,7 +8,6 @@ #include <utility> #include <vector> -#include "base/callback.h" #include "base/containers/flat_map.h" #include "base/files/file_path.h" #include "base/memory/read_only_shared_memory_region.h" @@ -126,7 +125,7 @@ PrepareCompositeRequest(const paint_preview::PaintPreviewProto& proto) { auto read_only_proto = ToReadOnlySharedMemory(proto); if (!read_only_proto) { - // TODO(crbug.com/1021590): Handle initialization errors. + DVLOG(1) << "Failed to read proto to read-only shared memory."; return nullptr; } begin_composite_request->proto = std::move(read_only_proto.value()); @@ -139,8 +138,11 @@ PlayerCompositorDelegate::PlayerCompositorDelegate( PaintPreviewBaseService* paint_preview_service, const GURL& expected_url, const DirectoryKey& key, + base::OnceClosure compositor_error, bool skip_service_launch) - : paint_preview_service_(paint_preview_service) { + : compositor_error_(std::move(compositor_error)), + paint_preview_service_(paint_preview_service), + key_(key) { if (skip_service_launch) { paint_preview_service_->GetCapturedPaintPreviewProto( key, base::BindOnce(&PlayerCompositorDelegate::OnProtoAvailable, @@ -164,10 +166,17 @@ PlayerCompositorDelegate::PlayerCompositorDelegate( weak_factory_.GetWeakPtr())); } -PlayerCompositorDelegate::~PlayerCompositorDelegate() = default; +PlayerCompositorDelegate::~PlayerCompositorDelegate() { + paint_preview_service_->GetTaskRunner()->PostTask( + FROM_HERE, + base::BindOnce(base::IgnoreResult(&FileManager::CompressDirectory), + paint_preview_service_->GetFileManager(), key_)); +} void PlayerCompositorDelegate::OnCompositorServiceDisconnected() { - // TODO(crbug.com/1039699): Handle compositor service disconnect event. + DVLOG(1) << "Compositor service disconnected."; + if (compositor_error_) + std::move(compositor_error_).Run(); } void PlayerCompositorDelegate::OnCompositorClientCreated( @@ -231,7 +240,9 @@ void PlayerCompositorDelegate::SendCompositeRequest( } void PlayerCompositorDelegate::OnCompositorClientDisconnected() { - // TODO(crbug.com/1039699): Handle compositor client disconnect event. + DVLOG(1) << "Compositor client disconnected."; + if (compositor_error_) + std::move(compositor_error_).Run(); } void PlayerCompositorDelegate::RequestBitmap( diff --git a/chromium/components/paint_preview/player/player_compositor_delegate.h b/chromium/components/paint_preview/player/player_compositor_delegate.h index 9759331fe44..372d568ee38 100644 --- a/chromium/components/paint_preview/player/player_compositor_delegate.h +++ b/chromium/components/paint_preview/player/player_compositor_delegate.h @@ -5,7 +5,7 @@ #ifndef COMPONENTS_PAINT_PREVIEW_PLAYER_PLAYER_COMPOSITOR_DELEGATE_H_ #define COMPONENTS_PAINT_PREVIEW_PLAYER_PLAYER_COMPOSITOR_DELEGATE_H_ -#include "base/callback_forward.h" +#include "base/callback.h" #include "base/containers/flat_map.h" #include "base/memory/weak_ptr.h" #include "base/optional.h" @@ -32,6 +32,7 @@ class PlayerCompositorDelegate { PlayerCompositorDelegate(PaintPreviewBaseService* paint_preview_service, const GURL& url, const DirectoryKey& key, + base::OnceClosure compositor_error, bool skip_service_launch = false); virtual ~PlayerCompositorDelegate(); @@ -55,6 +56,9 @@ class PlayerCompositorDelegate { std::vector<const GURL*> OnClick(const base::UnguessableToken& frame_guid, const gfx::Rect& rect); + protected: + base::OnceClosure compositor_error_; + private: void OnCompositorServiceDisconnected(); @@ -68,6 +72,7 @@ class PlayerCompositorDelegate { mojom::PaintPreviewBeginCompositeRequestPtr begin_composite_request); PaintPreviewBaseService* paint_preview_service_; + DirectoryKey key_; std::unique_ptr<PaintPreviewCompositorService> paint_preview_compositor_service_; std::unique_ptr<PaintPreviewCompositorClient> diff --git a/chromium/components/paint_preview/player/player_compositor_delegate_unittest.cc b/chromium/components/paint_preview/player/player_compositor_delegate_unittest.cc index d37bd8c5473..66ff43daaa2 100644 --- a/chromium/components/paint_preview/player/player_compositor_delegate_unittest.cc +++ b/chromium/components/paint_preview/player/player_compositor_delegate_unittest.cc @@ -8,6 +8,7 @@ #include "base/callback.h" #include "base/files/file_path.h" +#include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "base/memory/scoped_refptr.h" #include "base/run_loop.h" @@ -79,21 +80,58 @@ TEST(PlayerCompositorDelegate, OnClick) { loop.QuitClosure(), file_manager, proto, key)); loop.Run(); - PlayerCompositorDelegate player_compositor_delegate(&service, url, key, true); + { + PlayerCompositorDelegate player_compositor_delegate( + &service, url, key, base::DoNothing(), true); + env.RunUntilIdle(); + + auto res = player_compositor_delegate.OnClick(root_frame_id, + gfx::Rect(10, 20, 1, 1)); + ASSERT_EQ(res.size(), 1U); + EXPECT_EQ(*(res[0]), root_frame_link); + + res = player_compositor_delegate.OnClick(root_frame_id, + gfx::Rect(0, 0, 1, 1)); + EXPECT_TRUE(res.empty()); + + res = + player_compositor_delegate.OnClick(subframe_id, gfx::Rect(1, 2, 1, 1)); + ASSERT_EQ(res.size(), 1U); + EXPECT_EQ(*(res[0]), subframe_link); + } env.RunUntilIdle(); +} - auto res = player_compositor_delegate.OnClick(root_frame_id, - gfx::Rect(10, 20, 1, 1)); - ASSERT_EQ(res.size(), 1U); - EXPECT_EQ(*(res[0]), root_frame_link); - - res = - player_compositor_delegate.OnClick(root_frame_id, gfx::Rect(0, 0, 1, 1)); - EXPECT_TRUE(res.empty()); +TEST(PlayerCompositorDelegate, CompressOnClose) { + base::test::TaskEnvironment env; + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); - res = player_compositor_delegate.OnClick(subframe_id, gfx::Rect(1, 2, 1, 1)); - ASSERT_EQ(res.size(), 1U); - EXPECT_EQ(*(res[0]), subframe_link); + PaintPreviewBaseService service(temp_dir.GetPath(), "test", nullptr, false); + auto file_manager = service.GetFileManager(); + auto key = file_manager->CreateKey(1U); + base::FilePath dir; + file_manager->GetTaskRunner()->PostTaskAndReplyWithResult( + FROM_HERE, + base::BindOnce(&FileManager::CreateOrGetDirectory, file_manager, key, + false), + base::BindOnce( + [](base::FilePath* out, + const base::Optional<base::FilePath>& file_path) { + *out = file_path.value(); + }, + base::Unretained(&dir))); + env.RunUntilIdle(); + std::string data = "foo"; + EXPECT_TRUE( + base::WriteFile(dir.AppendASCII("test_file"), data.data(), data.size())); + { + PlayerCompositorDelegate player_compositor_delegate( + &service, GURL(), key, base::DoNothing(), true); + env.RunUntilIdle(); + } + env.RunUntilIdle(); + EXPECT_TRUE(base::PathExists(dir.AddExtensionASCII(".zip"))); } } // namespace paint_preview diff --git a/chromium/components/paint_preview/renderer/paint_preview_recorder_browsertest.cc b/chromium/components/paint_preview/renderer/paint_preview_recorder_browsertest.cc index bcbf397ad37..fdfe4c8032d 100644 --- a/chromium/components/paint_preview/renderer/paint_preview_recorder_browsertest.cc +++ b/chromium/components/paint_preview/renderer/paint_preview_recorder_browsertest.cc @@ -101,7 +101,9 @@ TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureMainFrameAndClipping) { base::Unretained(&out_response))); content::RunAllTasksUntilIdle(); - EXPECT_FALSE(out_response->embedding_token.has_value()); + EXPECT_TRUE(out_response->embedding_token.has_value()); + EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(), + out_response->embedding_token.value()); EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U); EXPECT_EQ(out_response->links.size(), 1U); @@ -177,7 +179,9 @@ TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureMainFrameWithScroll) { base::Unretained(&out_response))); content::RunAllTasksUntilIdle(); - EXPECT_FALSE(out_response->embedding_token.has_value()); + EXPECT_TRUE(out_response->embedding_token.has_value()); + EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(), + out_response->embedding_token.value()); EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U); // Relaxed checks on dimensions and no checks on positions. This is not @@ -233,7 +237,9 @@ TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureFragment) { &out_response)); content::RunAllTasksUntilIdle(); - EXPECT_FALSE(out_response->embedding_token.has_value()); + EXPECT_TRUE(out_response->embedding_token.has_value()); + EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(), + out_response->embedding_token.value()); EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U); EXPECT_EQ(out_response->links.size(), 1U); @@ -293,7 +299,9 @@ TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureMainFrameAndLocalFrame) { base::Unretained(&out_response))); content::RunAllTasksUntilIdle(); - EXPECT_FALSE(out_response->embedding_token.has_value()); + EXPECT_TRUE(out_response->embedding_token.has_value()); + EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(), + out_response->embedding_token.value()); EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U); } @@ -327,7 +335,7 @@ TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureLocalFrame) { base::Unretained(&out_response))); content::RunAllTasksUntilIdle(); - EXPECT_FALSE(out_response->embedding_token.has_value()); + EXPECT_TRUE(out_response->embedding_token.has_value()); EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U); } diff --git a/chromium/components/paint_preview/renderer/paint_preview_recorder_impl.cc b/chromium/components/paint_preview/renderer/paint_preview_recorder_impl.cc index c0cd610d353..95081f0f945 100644 --- a/chromium/components/paint_preview/renderer/paint_preview_recorder_impl.cc +++ b/chromium/components/paint_preview/renderer/paint_preview_recorder_impl.cc @@ -155,9 +155,13 @@ void PaintPreviewRecorderImpl::CapturePaintPreviewInternal( bounds = gfx::Rect(params->clip_rect.size()); } - cc::PaintRecorder recorder; auto tracker = std::make_unique<PaintPreviewTracker>( params->guid, frame->GetEmbeddingToken(), is_main_frame_); + auto size = frame->GetScrollOffset(); + tracker->SetScrollForFrame(SkISize::Make(size.width, size.height)); + response->scroll_offsets = gfx::Size(size.width, size.height); + + cc::PaintRecorder recorder; cc::PaintCanvas* canvas = recorder.beginRecording(bounds.width(), bounds.height()); canvas->SetPaintPreviewTracker(tracker.get()); @@ -169,7 +173,8 @@ void PaintPreviewRecorderImpl::CapturePaintPreviewInternal( // slow). base::TimeTicks start_time = base::TimeTicks::Now(); TRACE_EVENT_BEGIN0("paint_preview", "WebLocalFrame::CapturePaintPreview"); - bool success = frame->CapturePaintPreview(bounds, canvas); + bool success = frame->CapturePaintPreview( + bounds, canvas, /*include_linked_destinations=*/true); TRACE_EVENT_END0("paint_preview", "WebLocalFrame::CapturePaintPreview"); base::TimeDelta capture_time = base::TimeTicks::Now() - start_time; response->blink_recording_time = capture_time; @@ -202,18 +207,12 @@ void PaintPreviewRecorderImpl::CapturePaintPreviewInternal( return; } - base::ThreadPool::PostTaskAndReplyWithResult( - FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()}, - base::BindOnce(&FinishRecording, recorder.finishRecordingAsPicture(), - bounds, std::move(tracker), std::move(params->file), - params->max_capture_size, std::move(response)), - base::BindOnce( - [](CapturePaintPreviewCallback callback, - FinishedRecording recording) { - std::move(callback).Run(recording.status, - std::move(recording.response)); - }, - std::move(callback))); + // This cannot be done async if the recording contains a GPU accelerated + // image. + FinishedRecording recording = FinishRecording( + recorder.finishRecordingAsPicture(), bounds, std::move(tracker), + std::move(params->file), params->max_capture_size, std::move(response)); + std::move(callback).Run(recording.status, std::move(recording.response)); } } // namespace paint_preview diff --git a/chromium/components/paint_preview/renderer/paint_preview_recorder_utils.cc b/chromium/components/paint_preview/renderer/paint_preview_recorder_utils.cc index 2386c202a6c..caa501fc937 100644 --- a/chromium/components/paint_preview/renderer/paint_preview_recorder_utils.cc +++ b/chromium/components/paint_preview/renderer/paint_preview_recorder_utils.cc @@ -48,7 +48,7 @@ bool SerializeAsSkPicture(sk_sp<const cc::PaintRecord> record, auto skp = ToSkPicture( record, SkRect::MakeWH(dimensions.width(), dimensions.height()), nullptr, custom_callback); - if (!skp) + if (!skp || skp->cullRect().width() == 0 || skp->cullRect().height() == 0) return false; TypefaceSerializationContext typeface_context(tracker->GetTypefaceUsageMap()); |