| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/ambient/ambient_photo_controller.h" |
| |
| #include <array> |
| #include <memory> |
| #include <tuple> |
| #include <utility> |
| |
| #include "ash/ambient/ambient_constants.h" |
| #include "ash/ambient/ambient_controller.h" |
| #include "ash/ambient/ambient_photo_cache.h" |
| #include "ash/ambient/ambient_ui_settings.h" |
| #include "ash/ambient/model/ambient_animation_photo_config.h" |
| #include "ash/ambient/model/ambient_backend_model.h" |
| #include "ash/ambient/model/ambient_backend_model_observer.h" |
| #include "ash/ambient/model/ambient_photo_config.h" |
| #include "ash/ambient/test/ambient_ash_test_base.h" |
| #include "ash/ambient/test/ambient_test_util.h" |
| #include "ash/ambient/test/mock_ambient_backend_model_observer.h" |
| #include "ash/public/cpp/ambient/ambient_backend_controller.h" |
| #include "ash/public/cpp/ambient/fake_ambient_backend_controller_impl.h" |
| #include "ash/public/cpp/ambient/proto/photo_cache_entry.pb.h" |
| #include "ash/shell.h" |
| #include "ash/test/ash_test_util.h" |
| #include "ash/webui/personalization_app/mojom/personalization_app.mojom-shared.h" |
| #include "base/barrier_closure.h" |
| #include "base/base_paths.h" |
| #include "base/check.h" |
| #include "base/containers/contains.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/hash/sha1.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/scoped_observation.h" |
| #include "base/system/sys_info.h" |
| #include "base/test/bind.h" |
| #include "base/test/scoped_run_loop_timeout.h" |
| #include "base/time/time.h" |
| #include "base/timer/timer.h" |
| #include "cc/paint/skottie_resource_metadata.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "ui/gfx/image/image_skia.h" |
| |
| namespace ash { |
| |
| using ::testing::AnyOf; |
| using ::testing::Contains; |
| using ::testing::Eq; |
| using ::testing::IsEmpty; |
| using ::testing::Not; |
| using ::testing::Pointwise; |
| using ::testing::SizeIs; |
| |
| namespace { |
| |
| bool AreBackedBySameImage(const PhotoWithDetails& topic_l, |
| const PhotoWithDetails& topic_r) { |
| return !topic_l.photo.isNull() && !topic_r.photo.isNull() && |
| topic_l.photo.BackedBySameObjectAs(topic_r.photo); |
| } |
| |
| MATCHER(BackedBySameImage, "") { |
| return AreBackedBySameImage(std::get<0>(arg), std::get<1>(arg)); |
| } |
| |
| MATCHER_P(BackedBySameImageAs, photo_with_details, "") { |
| return AreBackedBySameImage(arg, photo_with_details); |
| } |
| |
| } // namespace |
| |
| class AmbientPhotoControllerTest : public AmbientAshTestBase { |
| protected: |
| void SetUp() override { |
| AmbientAshTestBase::SetUp(); |
| // Force the `AmbientUiSettings` to be any setting that has photos, or |
| // `photo_controller()` will be null and the tests will crash. |
| SetAmbientTheme(personalization_app::mojom::AmbientTheme::kSlideshow); |
| // This is common to all AmbientPhotoConfigs and mimics real-world behavior: |
| // When OnImagesReady() is called, the UI synchronously starts rendering. |
| ON_CALL(images_ready_observer_, OnImagesReady) |
| .WillByDefault(::testing::Invoke([this]() { |
| photo_controller()->OnMarkerHit( |
| AmbientPhotoConfig::Marker::kUiStartRendering); |
| })); |
| images_ready_observation_.Observe( |
| photo_controller()->ambient_backend_model()); |
| } |
| |
| void TearDown() override { |
| images_ready_observation_.Reset(); |
| AmbientAshTestBase::TearDown(); |
| } |
| |
| std::vector<int> GetSavedCacheIndices(bool backup = false) { |
| std::vector<int> result; |
| const auto& map = backup ? GetBackupCachedFiles() : GetCachedFiles(); |
| for (auto& it : map) { |
| result.push_back(it.first); |
| } |
| return result; |
| } |
| |
| const ::ambient::PhotoCacheEntry* GetCacheEntryAtIndex(int cache_index, |
| bool backup = false) { |
| const auto& files = backup ? GetBackupCachedFiles() : GetCachedFiles(); |
| auto it = files.find(cache_index); |
| if (it == files.end()) |
| return nullptr; |
| else |
| return &(it->second); |
| } |
| |
| void WriteCacheDataBlocking(int cache_index, |
| std::string image, |
| const std::string* details = nullptr, |
| const std::string* related_image = nullptr, |
| const std::string* related_details = nullptr, |
| bool is_portrait = false) { |
| ::ambient::PhotoCacheEntry cache_entry; |
| cache_entry.mutable_primary_photo()->set_image(std::move(image)); |
| |
| if (details) |
| cache_entry.mutable_primary_photo()->set_details(*details); |
| |
| cache_entry.mutable_primary_photo()->set_is_portrait(is_portrait); |
| |
| if (related_image) { |
| cache_entry.mutable_related_photo()->set_image(*related_image); |
| cache_entry.mutable_related_photo()->set_is_portrait(is_portrait); |
| } |
| |
| if (related_details) |
| cache_entry.mutable_related_photo()->set_details(*related_details); |
| |
| base::RunLoop loop; |
| ambient_photo_cache::WritePhotoCache(ambient_photo_cache::Store::kPrimary, |
| /*cache_index=*/cache_index, |
| cache_entry, loop.QuitClosure()); |
| loop.Run(); |
| } |
| |
| void ScheduleFetchBackupImages() { |
| photo_controller()->ScheduleFetchBackupImages(); |
| } |
| |
| void Init() { photo_controller()->Init(); } |
| |
| bool RunUntilImagesReady() { |
| if (photo_controller()->ambient_backend_model()->ImagesReady()) { |
| return true; |
| } |
| |
| static constexpr base::TimeDelta kTimeout = base::Seconds(3); |
| base::RunLoop loop; |
| base::RepeatingClosure quit_closure = loop.QuitClosure(); |
| testing::NiceMock<MockAmbientBackendModelObserver> mock_backend_observer; |
| base::ScopedObservation<AmbientBackendModel, AmbientBackendModelObserver> |
| scoped_observation{&mock_backend_observer}; |
| scoped_observation.Observe(photo_controller()->ambient_backend_model()); |
| bool images_ready = false; |
| ON_CALL(mock_backend_observer, OnImagesReady) |
| .WillByDefault(::testing::Invoke([quit_closure, &images_ready]() { |
| quit_closure.Run(); |
| images_ready = true; |
| })); |
| task_environment()->GetMainThreadTaskRunner()->PostDelayedTask( |
| FROM_HERE, quit_closure, kTimeout); |
| loop.Run(); |
| return images_ready; |
| } |
| |
| bool RunUntilNextTopicsAdded(int num_expected_topics) { |
| static constexpr base::TimeDelta kTimeout = base::Seconds(3); |
| base::RunLoop loop; |
| base::RepeatingClosure quit_closure = loop.QuitClosure(); |
| int num_topics_added = 0; |
| testing::NiceMock<MockAmbientBackendModelObserver> mock_backend_observer; |
| base::ScopedObservation<AmbientBackendModel, AmbientBackendModelObserver> |
| scoped_observation{&mock_backend_observer}; |
| scoped_observation.Observe(photo_controller()->ambient_backend_model()); |
| ON_CALL(mock_backend_observer, OnImageAdded) |
| .WillByDefault(::testing::Invoke( |
| [quit_closure, num_expected_topics, &num_topics_added]() { |
| ++num_topics_added; |
| if (num_topics_added >= num_expected_topics) |
| quit_closure.Run(); |
| })); |
| task_environment()->GetMainThreadTaskRunner()->PostDelayedTask( |
| FROM_HERE, quit_closure, kTimeout); |
| loop.Run(); |
| return num_topics_added >= num_expected_topics; |
| } |
| |
| testing::NiceMock<MockAmbientBackendModelObserver> images_ready_observer_; |
| base::ScopedObservation<AmbientBackendModel, AmbientBackendModelObserver> |
| images_ready_observation_{&images_ready_observer_}; |
| }; |
| |
| // Has 2 positions in the animation for photos and 2 dynamic assets per |
| // position. |
| class AmbientPhotoControllerAnimationTest : public AmbientPhotoControllerTest { |
| protected: |
| void SetUp() override { |
| AmbientPhotoControllerTest::SetUp(); |
| |
| cc::SkottieResourceMetadataMap resource_metadata; |
| std::array<std::string, 4> all_dynamic_asset_ids = { |
| GenerateLottieDynamicAssetIdForTesting(/*position=*/"A", /*idx=*/1), |
| GenerateLottieDynamicAssetIdForTesting(/*position=*/"A", /*idx=*/2), |
| GenerateLottieDynamicAssetIdForTesting(/*position=*/"B", /*idx=*/1), |
| GenerateLottieDynamicAssetIdForTesting(/*position=*/"B", /*idx=*/2)}; |
| for (const std::string& asset_id : all_dynamic_asset_ids) { |
| CHECK(resource_metadata.RegisterAsset("test-path", "test-name", asset_id, |
| /*size=*/std::nullopt)); |
| } |
| |
| photo_controller()->ambient_backend_model()->SetPhotoConfig( |
| CreateAmbientAnimationPhotoConfig(resource_metadata)); |
| CHECK_EQ(photo_config().GetNumDecodedTopicsToBuffer(), 4u); |
| CHECK_EQ(photo_config().topic_set_size, 2u); |
| } |
| |
| const AmbientPhotoConfig& photo_config() { |
| return photo_controller()->ambient_backend_model()->photo_config(); |
| } |
| }; |
| |
| // No topics should be prepared at all; the screensaver doesn't have photos in |
| // it. AmbientPhotoController should be completely idle and |
| // AmbientBackendModel::ImagesReady() should be true immediately. |
| class AmbientPhotoControllerEmptyConfigTest |
| : public AmbientPhotoControllerTest { |
| protected: |
| void SetUp() override { |
| AmbientPhotoControllerTest::SetUp(); |
| photo_controller()->ambient_backend_model()->SetPhotoConfig( |
| AmbientPhotoConfig()); |
| } |
| }; |
| |
| // Test that topics are downloaded when starting screen update. |
| TEST_F(AmbientPhotoControllerTest, ShouldStartToDownloadTopics) { |
| auto topics = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| EXPECT_TRUE(topics.empty()); |
| |
| // Start to refresh images. |
| photo_controller()->StartScreenUpdate(); |
| topics = photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| EXPECT_TRUE(topics.empty()); |
| |
| ASSERT_TRUE(RunUntilImagesReady()); |
| topics = photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| EXPECT_FALSE(topics.empty()); |
| |
| // Stop to refresh images. |
| photo_controller()->StopScreenUpdate(); |
| topics = photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| EXPECT_TRUE(topics.empty()); |
| } |
| |
| // Test that image is downloaded when starting screen update. |
| TEST_F(AmbientPhotoControllerTest, ShouldStartToDownloadImages) { |
| PhotoWithDetails image; |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image); |
| EXPECT_TRUE(image.IsNull()); |
| |
| // Start to refresh images. |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image); |
| EXPECT_FALSE(image.IsNull()); |
| |
| // Stop to refresh images. |
| photo_controller()->StopScreenUpdate(); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image); |
| EXPECT_TRUE(image.IsNull()); |
| } |
| |
| // Tests that photos are updated when OnMarkerHit() is called. |
| TEST_F(AmbientPhotoControllerTest, OnMarkerHitShouldUpdatePhoto) { |
| PhotoWithDetails image1; |
| PhotoWithDetails image2; |
| PhotoWithDetails image3; |
| |
| // Start to refresh images. |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image1); |
| EXPECT_FALSE(image1.IsNull()); |
| EXPECT_TRUE(image2.IsNull()); |
| |
| photo_controller()->OnMarkerHit(AmbientPhotoConfig::Marker::kUiCycleEnded); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(/*num_expected_topics=*/1)); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image2); |
| EXPECT_FALSE(image2.IsNull()); |
| EXPECT_FALSE(image1.photo.BackedBySameObjectAs(image2.photo)); |
| EXPECT_TRUE(image3.IsNull()); |
| |
| photo_controller()->OnMarkerHit(AmbientPhotoConfig::Marker::kUiCycleEnded); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(/*num_expected_topics=*/1)); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image3); |
| EXPECT_FALSE(image3.IsNull()); |
| EXPECT_FALSE(image1.photo.BackedBySameObjectAs(image3.photo)); |
| EXPECT_FALSE(image2.photo.BackedBySameObjectAs(image3.photo)); |
| |
| // Stop to refresh images. |
| photo_controller()->StopScreenUpdate(); |
| } |
| |
| TEST_F(AmbientPhotoControllerTest, |
| ShouldLoadSavedTopicsFromDiskWithoutInternet) { |
| // Start ambient mode and run until ImagesReady(). At this point, the |
| // controller should have saved 2 topics to disk. |
| PhotoWithDetails image; |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image); |
| ASSERT_FALSE(image.IsNull()); |
| |
| // Stop ambient mode. That should clear the decoded topics in the model but |
| // not clear the saved topics on disk. |
| photo_controller()->StopScreenUpdate(); |
| |
| // Simulate internet connection down. |
| backend_controller()->SetFetchScreenUpdateInfoResponseSize( |
| /*num_topics_to_return=*/0); |
| |
| // Restart ambient mode, and it should load previously saved topics from disk. |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image); |
| EXPECT_FALSE(image.IsNull()); |
| } |
| |
| // Tests that image details is correctly set. |
| TEST_F(AmbientPhotoControllerTest, ShouldSetDetailsCorrectly) { |
| SetPhotoOrientation(/*portrait=*/true); |
| // Start to refresh images. |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| PhotoWithDetails image; |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image); |
| EXPECT_FALSE(image.IsNull()); |
| |
| // Fake details defined in fake_ambient_backend_controller_impl.cc. |
| EXPECT_EQ(image.details, "fake-photo-attribution"); |
| |
| // Stop to refresh images. |
| photo_controller()->StopScreenUpdate(); |
| } |
| |
| // Test that image is saved. |
| TEST_F(AmbientPhotoControllerTest, ShouldSaveImagesOnDisk) { |
| // Start to refresh images. It will download two images immediately and write |
| // them in |ambient_image_path|. It will also download one more image after |
| // OnMarkerHit(). It will also download the related images and not cache |
| // them. |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| photo_controller()->OnMarkerHit(AmbientPhotoConfig::Marker::kUiCycleEnded); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(/*num_expected_topics=*/1)); |
| |
| // Count number of writes to cache. There should be three cache writes during |
| // this ambient mode session. |
| auto file_paths = GetSavedCacheIndices(); |
| EXPECT_EQ(file_paths.size(), 3u); |
| } |
| |
| // Test that image is save and will not be deleted when stopping ambient mode. |
| TEST_F(AmbientPhotoControllerTest, ShouldNotDeleteImagesOnDisk) { |
| // Start to refresh images. It will download two images immediately and write |
| // them in |ambient_image_path|. It will also download one more image after |
| // OnMarkerHit(). It will also download the related images and not cache |
| // them. |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| photo_controller()->OnMarkerHit(AmbientPhotoConfig::Marker::kUiCycleEnded); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(/*num_expected_topics=*/1)); |
| |
| EXPECT_EQ(GetSavedCacheIndices().size(), 3u); |
| |
| PhotoWithDetails image; |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image); |
| EXPECT_FALSE(image.IsNull()); |
| |
| // Stop to refresh images. |
| photo_controller()->StopScreenUpdate(); |
| FastForwardByPhotoRefreshInterval(); |
| |
| EXPECT_EQ(GetSavedCacheIndices().size(), 3u); |
| |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image); |
| EXPECT_TRUE(image.IsNull()); |
| } |
| |
| // Test that image is read from disk when no more topics. |
| TEST_F(AmbientPhotoControllerTest, ShouldReadCacheWhenNoMoreTopics) { |
| backend_controller()->SetFetchScreenUpdateInfoResponseSize(0); |
| Init(); |
| FetchImage(); |
| FastForwardByPhotoRefreshInterval(); |
| // Topics is empty. Will read from cache, which is empty. |
| PhotoWithDetails image; |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/&image, |
| /*next_image=*/nullptr); |
| EXPECT_TRUE(image.IsNull()); |
| |
| // Save a file to check if it gets read for display. |
| WriteCacheDataBlocking(/*cache_index=*/0, |
| CreateEncodedImageForTesting(gfx::Size(10, 10))); |
| |
| // Reset variables in photo controller. |
| Init(); |
| FetchImage(); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(/*num_expected_topics=*/1)); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/&image, |
| /*next_image=*/nullptr); |
| EXPECT_FALSE(image.IsNull()); |
| } |
| |
| // Test that will try 100 times to read image from disk when no more topics. |
| TEST_F(AmbientPhotoControllerTest, |
| ShouldTry100TimesToReadCacheWhenNoMoreTopics) { |
| backend_controller()->SetFetchScreenUpdateInfoResponseSize(0); |
| Init(); |
| FetchImage(); |
| FastForwardByPhotoRefreshInterval(); |
| // Topics is empty. Will read from cache, which is empty. |
| PhotoWithDetails image; |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/&image, |
| /*next_image=*/nullptr); |
| EXPECT_TRUE(image.IsNull()); |
| |
| // The initial file name to be read is 0. Save a file with index 99 to check |
| // if it gets read for display. |
| WriteCacheDataBlocking(/*cache_index=*/99, |
| CreateEncodedImageForTesting(gfx::Size(10, 10))); |
| |
| // Reset variables in photo controller. |
| Init(); |
| FetchImage(); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(/*num_expected_topics=*/1)); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/&image, |
| /*next_image=*/nullptr); |
| EXPECT_FALSE(image.IsNull()); |
| } |
| |
| // Test that image is read from disk when image downloading failed. |
| TEST_F(AmbientPhotoControllerTest, ShouldReadCacheWhenImageDownloadingFailed) { |
| SetDownloadPhotoData(""); |
| Init(); |
| FetchTopics(); |
| // Forward a little bit time. FetchTopics() will succeed. Downloading should |
| // fail. Will read from cache, which is empty. |
| task_environment()->FastForwardBy(0.2 * kTopicFetchInterval); |
| PhotoWithDetails image; |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/&image, |
| /*next_image=*/nullptr); |
| EXPECT_TRUE(image.IsNull()); |
| |
| // Save a file to check if it gets read for display. |
| WriteCacheDataBlocking(/*cache_index=*/0, |
| CreateEncodedImageForTesting(gfx::Size(10, 10))); |
| |
| // Reset variables in photo controller. |
| Init(); |
| FetchTopics(); |
| // Forward a little bit time. FetchTopics() will succeed. Downloading should |
| // fail. Will read from cache. |
| task_environment()->FastForwardBy(0.2 * kTopicFetchInterval); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/&image, |
| /*next_image=*/nullptr); |
| EXPECT_FALSE(image.IsNull()); |
| } |
| |
| // Test that image details is read from disk. |
| TEST_F(AmbientPhotoControllerTest, ShouldPopulateDetailsWhenReadFromCache) { |
| backend_controller()->SetFetchScreenUpdateInfoResponseSize(0); |
| Init(); |
| FetchImage(); |
| FastForwardByPhotoRefreshInterval(); |
| // Topics is empty. Will read from cache, which is empty. |
| PhotoWithDetails image; |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/&image, |
| /*next_image=*/nullptr); |
| EXPECT_TRUE(image.IsNull()); |
| |
| // Save a file to check if it gets read for display. |
| std::string details("image details"); |
| WriteCacheDataBlocking(/*cache_index=*/0, |
| CreateEncodedImageForTesting(gfx::Size(10, 10)), |
| &details); |
| |
| // Reset variables in photo controller. |
| Init(); |
| FetchImage(); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(/*num_expected_topics=*/1)); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/&image, |
| /*next_image=*/nullptr); |
| EXPECT_FALSE(image.IsNull()); |
| EXPECT_EQ(image.details, details); |
| } |
| |
| // Test that image is read from disk when image decoding failed. |
| TEST_F(AmbientPhotoControllerTest, ShouldReadCacheWhenImageDecodingFailed) { |
| WriteCacheDataBlocking(/*cache_index=*/0, |
| CreateEncodedImageForTesting(gfx::Size(10, 10))); |
| WriteCacheDataBlocking(/*cache_index=*/1, |
| CreateEncodedImageForTesting(gfx::Size(20, 20))); |
| |
| SetDownloadPhotoData("invalid-image-data"); |
| |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| photo_controller()->StopScreenUpdate(); |
| |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| photo_controller()->StopScreenUpdate(); |
| } |
| |
| // Test that image will refresh when have more topics. |
| TEST_F(AmbientPhotoControllerTest, ShouldResumeWhenHaveMoreTopics) { |
| backend_controller()->SetFetchScreenUpdateInfoResponseSize(0); |
| Init(); |
| FetchImage(); |
| task_environment()->RunUntilIdle(); |
| // Topics is empty. Will read from cache, which is empty. |
| PhotoWithDetails image; |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image); |
| EXPECT_TRUE(image.IsNull()); |
| |
| // Backend starts returning topics again, so the `AmbientTopicQueue` should |
| // not longer be empty. |
| backend_controller()->SetFetchScreenUpdateInfoResponseSize(kTopicsBatchSize); |
| task_environment()->FastForwardBy(kTopicFetchInterval); |
| |
| FetchTopics(); |
| task_environment()->RunUntilIdle(); |
| photo_controller()->ambient_backend_model()->GetCurrentAndNextImages( |
| /*current_image=*/nullptr, |
| /*next_image=*/&image); |
| EXPECT_FALSE(image.IsNull()); |
| } |
| |
| TEST_F(AmbientPhotoControllerTest, ShouldDownloadBackupImagesWhenScheduled) { |
| SetDownloadPhotoDataForUrl( |
| GURL(backend_controller()->GetBackupPhotoUrls()[0]), |
| CreateEncodedImageForTesting(gfx::Size(10, 10))); |
| SetDownloadPhotoDataForUrl( |
| GURL(backend_controller()->GetBackupPhotoUrls()[1]), |
| CreateEncodedImageForTesting(gfx::Size(20, 20))); |
| |
| ScheduleFetchBackupImages(); |
| |
| EXPECT_TRUE( |
| photo_controller()->backup_photo_refresh_timer_for_testing().IsRunning()); |
| |
| // Timer is running but download has not started yet. |
| EXPECT_TRUE(GetSavedCacheIndices(/*backup=*/true).empty()); |
| task_environment()->FastForwardBy(kBackupPhotoRefreshDelay); |
| |
| // Timer should have stopped. |
| EXPECT_FALSE( |
| photo_controller()->backup_photo_refresh_timer_for_testing().IsRunning()); |
| |
| // Should have been two cache writes to backup data. |
| const auto& backup_data = GetBackupCachedFiles(); |
| ASSERT_EQ(backup_data.size(), 2u); |
| ASSERT_TRUE(base::Contains(backup_data, 0)); |
| ASSERT_TRUE(base::Contains(backup_data, 1)); |
| for (const auto& i : backup_data) { |
| EXPECT_TRUE(i.second.primary_photo().details().empty()); |
| EXPECT_TRUE(i.second.related_photo().image().empty()); |
| EXPECT_TRUE(i.second.related_photo().details().empty()); |
| } |
| } |
| |
| TEST_F(AmbientPhotoControllerTest, ShouldResetTimerWhenBackupImagesFail) { |
| ScheduleFetchBackupImages(); |
| |
| EXPECT_TRUE( |
| photo_controller()->backup_photo_refresh_timer_for_testing().IsRunning()); |
| |
| // Simulate an error in DownloadToFile. |
| SetDownloadPhotoDataForUrl( |
| GURL(backend_controller()->GetBackupPhotoUrls()[0]), ""); |
| SetDownloadPhotoDataForUrl( |
| GURL(backend_controller()->GetBackupPhotoUrls()[1]), ""); |
| task_environment()->FastForwardBy(kBackupPhotoRefreshDelay); |
| |
| EXPECT_TRUE(GetBackupCachedFiles().empty()); |
| |
| // Timer should have restarted. |
| EXPECT_TRUE( |
| photo_controller()->backup_photo_refresh_timer_for_testing().IsRunning()); |
| } |
| |
| TEST_F(AmbientPhotoControllerTest, |
| ShouldStartDownloadBackupImagesOnAmbientModeStart) { |
| ScheduleFetchBackupImages(); |
| |
| EXPECT_TRUE( |
| photo_controller()->backup_photo_refresh_timer_for_testing().IsRunning()); |
| |
| SetDownloadPhotoDataForUrl( |
| GURL(backend_controller()->GetBackupPhotoUrls()[0]), |
| CreateEncodedImageForTesting(gfx::Size(10, 10))); |
| SetDownloadPhotoDataForUrl( |
| GURL(backend_controller()->GetBackupPhotoUrls()[1]), |
| CreateEncodedImageForTesting(gfx::Size(20, 20))); |
| |
| photo_controller()->StartScreenUpdate(); |
| |
| // Download should have started immediately. |
| EXPECT_FALSE( |
| photo_controller()->backup_photo_refresh_timer_for_testing().IsRunning()); |
| |
| task_environment()->RunUntilIdle(); |
| |
| // Download has triggered and backup cache directory is created. Should be |
| // two cache writes to backup cache. |
| const auto& backup_data = GetBackupCachedFiles(); |
| ASSERT_EQ(backup_data.size(), 2u); |
| ASSERT_TRUE(base::Contains(backup_data, 0)); |
| ASSERT_TRUE(base::Contains(backup_data, 1)); |
| for (const auto& i : backup_data) { |
| EXPECT_TRUE(i.second.primary_photo().details().empty()); |
| EXPECT_TRUE(i.second.related_photo().image().empty()); |
| EXPECT_TRUE(i.second.related_photo().details().empty()); |
| } |
| } |
| |
| TEST_F(AmbientPhotoControllerTest, UsesBackupCacheAfterPrimaryCacheCleared) { |
| ScheduleFetchBackupImages(); |
| |
| photo_controller()->StartScreenUpdate(); |
| task_environment()->RunUntilIdle(); |
| |
| photo_controller()->StopScreenUpdate(); |
| |
| // At this point, both the primary and backup cache should be filled with |
| // photos from the last "screen update". ClearCache() should only clear the |
| // primary cache, leaving photos in the backup cache to use. |
| ASSERT_FALSE(GetBackupCachedFiles().empty()); |
| ambient_photo_cache::Clear(ambient_photo_cache::Store::kPrimary); |
| // Simulate an IMAX failure to leave the photo controller no choice but to |
| // resort to the backup cache. |
| backend_controller()->SetFetchScreenUpdateInfoResponseSize(0); |
| |
| photo_controller()->StartScreenUpdate(); |
| // Running until OnImagesReady() ensures the backup photos were loaded and |
| // ambient UI can successfully start. |
| ASSERT_TRUE(RunUntilImagesReady()); |
| } |
| |
| TEST_F(AmbientPhotoControllerTest, ShouldNotLoadDuplicateImages) { |
| testing::NiceMock<MockAmbientBackendModelObserver> mock_backend_observer; |
| base::ScopedObservation<AmbientBackendModel, AmbientBackendModelObserver> |
| scoped_observation{&mock_backend_observer}; |
| |
| scoped_observation.Observe(photo_controller()->ambient_backend_model()); |
| |
| // All images downloaded will be identical. |
| std::string image_data = CreateEncodedImageForTesting(gfx::Size(10, 10)); |
| SetDownloadPhotoData(image_data); |
| |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(/*num_expected_topics=*/1)); |
| |
| // Should contain hash of downloaded data. |
| EXPECT_TRUE(photo_controller()->ambient_backend_model()->IsHashDuplicate( |
| base::SHA1HashString(image_data))); |
| // Only one image should have been loaded. |
| EXPECT_FALSE(photo_controller()->ambient_backend_model()->ImagesReady()); |
| |
| // Now expect a call because second image is loaded. |
| EXPECT_CALL(mock_backend_observer, OnImagesReady).Times(1); |
| std::string image_data_2 = CreateEncodedImageForTesting(gfx::Size(20, 20)); |
| SetDownloadPhotoData(image_data_2); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| |
| // Second image should have been loaded. |
| EXPECT_TRUE(photo_controller()->ambient_backend_model()->IsHashDuplicate( |
| base::SHA1HashString(image_data_2))); |
| EXPECT_TRUE(photo_controller()->ambient_backend_model()->ImagesReady()); |
| } |
| |
| TEST_F(AmbientPhotoControllerTest, IsScreenUpdateActive) { |
| ASSERT_FALSE(photo_controller()->IsScreenUpdateActive()); |
| photo_controller()->StartScreenUpdate(); |
| EXPECT_TRUE(photo_controller()->IsScreenUpdateActive()); |
| photo_controller()->StopScreenUpdate(); |
| EXPECT_FALSE(photo_controller()->IsScreenUpdateActive()); |
| } |
| |
| TEST_F(AmbientPhotoControllerAnimationTest, AnimationPreparesInitialTopicSet) { |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| EXPECT_THAT(photo_controller()->ambient_backend_model()->all_decoded_topics(), |
| SizeIs(photo_config().GetNumDecodedTopicsToBuffer())); |
| } |
| |
| TEST_F(AmbientPhotoControllerAnimationTest, |
| AnimationRefreshesTopicSetEachCycle) { |
| photo_controller()->StartScreenUpdate(); |
| // Animation starts rendering. This should trigger an image refresh. |
| ASSERT_TRUE(RunUntilImagesReady()); |
| base::circular_deque<PhotoWithDetails> old_photos = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(photo_config().topic_set_size)); |
| base::circular_deque<PhotoWithDetails> new_photos = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| EXPECT_THAT(new_photos, SizeIs(photo_config().GetNumDecodedTopicsToBuffer())); |
| ASSERT_THAT(old_photos.size(), Eq(new_photos.size())); |
| |
| // Verify that the new set actually has 2 new photos and 2 photos from the |
| // old set. |
| EXPECT_THAT(new_photos[0], BackedBySameImageAs(old_photos[2])); |
| EXPECT_THAT(new_photos[1], BackedBySameImageAs(old_photos[3])); |
| EXPECT_THAT(old_photos, Not(Contains(BackedBySameImageAs(new_photos[2])))); |
| EXPECT_THAT(old_photos, Not(Contains(BackedBySameImageAs(new_photos[3])))); |
| old_photos = new_photos; |
| |
| // Animation cycle ends and another image refresh starts. |
| photo_controller()->OnMarkerHit(AmbientPhotoConfig::Marker::kUiCycleEnded); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(photo_config().topic_set_size)); |
| new_photos = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| EXPECT_THAT(new_photos, SizeIs(photo_config().GetNumDecodedTopicsToBuffer())); |
| ASSERT_THAT(old_photos.size(), Eq(new_photos.size())); |
| EXPECT_THAT(new_photos[0], BackedBySameImageAs(old_photos[2])); |
| EXPECT_THAT(new_photos[1], BackedBySameImageAs(old_photos[3])); |
| EXPECT_THAT(old_photos, Not(Contains(BackedBySameImageAs(new_photos[2])))); |
| EXPECT_THAT(old_photos, Not(Contains(BackedBySameImageAs(new_photos[3])))); |
| } |
| |
| TEST_F(AmbientPhotoControllerAnimationTest, |
| StopsRefreshingImagesAfterTargetAmountBuffered) { |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(photo_config().topic_set_size)); |
| |
| // Fast forward time to make sure no more images are prepared after |
| // |kNumDynamicAssets| has been added. |
| base::circular_deque<PhotoWithDetails> old_photos = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| task_environment()->FastForwardBy(base::Minutes(1)); |
| base::circular_deque<PhotoWithDetails> new_photos = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| EXPECT_THAT(photo_controller()->ambient_backend_model()->all_decoded_topics(), |
| SizeIs(photo_config().GetNumDecodedTopicsToBuffer())); |
| EXPECT_THAT(new_photos, Pointwise(BackedBySameImage(), old_photos)); |
| } |
| |
| TEST_F(AmbientPhotoControllerAnimationTest, |
| AnimationRefreshesAfterIncompleteTopicSet) { |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| base::circular_deque<PhotoWithDetails> old_photos = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| |
| ASSERT_TRUE(RunUntilNextTopicsAdded(photo_config().topic_set_size / 2)); |
| base::circular_deque<PhotoWithDetails> new_photos = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| EXPECT_THAT(new_photos, SizeIs(photo_config().GetNumDecodedTopicsToBuffer())); |
| ASSERT_THAT(old_photos.size(), Eq(new_photos.size())); |
| |
| EXPECT_THAT(new_photos[0], BackedBySameImageAs(old_photos[1])); |
| EXPECT_THAT(new_photos[1], BackedBySameImageAs(old_photos[2])); |
| EXPECT_THAT(new_photos[2], BackedBySameImageAs(old_photos[3])); |
| EXPECT_THAT(old_photos, Not(Contains(BackedBySameImageAs(new_photos[3])))); |
| old_photos = new_photos; |
| |
| // Cycle ends when only half of target amount refreshed. |
| photo_controller()->OnMarkerHit(AmbientPhotoConfig::Marker::kUiCycleEnded); |
| ASSERT_TRUE(RunUntilNextTopicsAdded(photo_config().topic_set_size)); |
| // Fast forward time to make sure no more images are prepared after |
| // |photo_config().topic_set_size| has been added. |
| task_environment()->FastForwardBy(base::Minutes(1)); |
| new_photos = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| EXPECT_THAT(new_photos, SizeIs(photo_config().GetNumDecodedTopicsToBuffer())); |
| ASSERT_THAT(old_photos.size(), Eq(new_photos.size())); |
| EXPECT_THAT(new_photos[0], BackedBySameImageAs(old_photos[2])); |
| EXPECT_THAT(new_photos[1], BackedBySameImageAs(old_photos[3])); |
| EXPECT_THAT(old_photos, Not(Contains(BackedBySameImageAs(new_photos[2])))); |
| EXPECT_THAT(old_photos, Not(Contains(BackedBySameImageAs(new_photos[3])))); |
| } |
| |
| TEST_F(AmbientPhotoControllerAnimationTest, |
| HandlesMarkerWhenInitialTopicSetIncomplete) { |
| constexpr base::TimeDelta kPhotoDownloadDelay = base::Seconds(5); |
| constexpr base::TimeDelta kTimeoutAfterFirstPhoto = base::Seconds(10); |
| SetPhotoDownloadDelay(kPhotoDownloadDelay); |
| photo_controller()->StartScreenUpdate(); |
| task_environment()->FastForwardBy(kPhotoDownloadDelay + |
| kTimeoutAfterFirstPhoto); |
| ASSERT_TRUE(photo_controller()->ambient_backend_model()->ImagesReady()); |
| ASSERT_THAT(photo_controller()->ambient_backend_model()->all_decoded_topics(), |
| SizeIs(3)); |
| |
| // UI starts rendering when only 3/4 of the initial topic set is prepared. |
| // The controller should immediately start preparing another 2 topics (the |
| // size of 1 topic set). |
| task_environment()->FastForwardBy(kPhotoDownloadDelay * 2); |
| ASSERT_THAT(photo_controller()->ambient_backend_model()->all_decoded_topics(), |
| SizeIs(photo_config().GetNumDecodedTopicsToBuffer())); |
| // Fast forward time to make sure no more images are prepared after |
| // |photo_config().topic_set_size| has been added. |
| base::circular_deque<PhotoWithDetails> photos_before = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| task_environment()->FastForwardBy(base::Minutes(1)); |
| base::circular_deque<PhotoWithDetails> photos_after = |
| photo_controller()->ambient_backend_model()->all_decoded_topics(); |
| EXPECT_THAT(photos_after, Pointwise(BackedBySameImage(), photos_before)); |
| } |
| |
| TEST_F(AmbientPhotoControllerEmptyConfigTest, CallsOnImagesReadyImmediately) { |
| photo_controller()->StartScreenUpdate(); |
| ASSERT_TRUE(RunUntilImagesReady()); |
| EXPECT_THAT(photo_controller()->ambient_backend_model()->all_decoded_topics(), |
| IsEmpty()); |
| task_environment()->FastForwardBy(base::Minutes(1)); |
| EXPECT_TRUE(photo_controller()->ambient_backend_model()->ImagesReady()); |
| EXPECT_THAT(photo_controller()->ambient_backend_model()->all_decoded_topics(), |
| IsEmpty()); |
| } |
| |
| } // namespace ash |