| // 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. |
| |
| #include "ash/ambient/ambient_photo_controller.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "ash/ambient/ambient_constants.h" |
| #include "ash/ambient/ambient_controller.h" |
| #include "ash/ambient/model/ambient_backend_model.h" |
| #include "ash/ambient/model/ambient_backend_model_observer.h" |
| #include "ash/ambient/test/ambient_ash_test_base.h" |
| #include "ash/public/cpp/ambient/ambient_backend_controller.h" |
| #include "ash/public/cpp/ambient/fake_ambient_backend_controller_impl.h" |
| #include "ash/shell.h" |
| #include "base/barrier_closure.h" |
| #include "base/base_paths.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/hash/sha1.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/scoped_observation.h" |
| #include "base/stl_util.h" |
| #include "base/system/sys_info.h" |
| #include "base/test/bind.h" |
| #include "base/timer/timer.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "ui/gfx/image/image_skia.h" |
| |
| namespace ash { |
| |
| namespace { |
| class MockAmbientBackendModelObserver : public AmbientBackendModelObserver { |
| public: |
| MockAmbientBackendModelObserver() = default; |
| ~MockAmbientBackendModelObserver() override = default; |
| |
| // AmbientBackendModelObserver: |
| MOCK_METHOD(void, OnImageAdded, (), (override)); |
| MOCK_METHOD(void, OnImagesReady, (), (override)); |
| }; |
| |
| } // namespace |
| |
| class AmbientPhotoControllerTest : public AmbientAshTestBase { |
| public: |
| 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 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, |
| const std::string* image = nullptr, |
| const std::string* details = nullptr, |
| const std::string* related_image = nullptr) { |
| base::RunLoop loop; |
| photo_cache()->WriteFiles(/*cache_index=*/cache_index, /*image=*/image, |
| /*details=*/details, |
| /*related_image=*/related_image, |
| loop.QuitClosure()); |
| loop.Run(); |
| } |
| |
| void ScheduleFetchBackupImages() { |
| photo_controller()->ScheduleFetchBackupImages(); |
| } |
| }; |
| |
| // Test that topics are downloaded when starting screen update. |
| TEST_F(AmbientPhotoControllerTest, ShouldStartToDownloadTopics) { |
| auto topics = photo_controller()->ambient_backend_model()->topics(); |
| EXPECT_TRUE(topics.empty()); |
| |
| // Start to refresh images. |
| photo_controller()->StartScreenUpdate(); |
| topics = photo_controller()->ambient_backend_model()->topics(); |
| EXPECT_TRUE(topics.empty()); |
| |
| FastForwardToNextImage(); |
| topics = photo_controller()->ambient_backend_model()->topics(); |
| EXPECT_FALSE(topics.empty()); |
| |
| // Stop to refresh images. |
| photo_controller()->StopScreenUpdate(); |
| topics = photo_controller()->ambient_backend_model()->topics(); |
| EXPECT_TRUE(topics.empty()); |
| } |
| |
| // Test that image is downloaded when starting screen update. |
| TEST_F(AmbientPhotoControllerTest, ShouldStartToDownloadImages) { |
| auto image = photo_controller()->ambient_backend_model()->GetNextImage(); |
| EXPECT_TRUE(image.IsNull()); |
| |
| // Start to refresh images. |
| photo_controller()->StartScreenUpdate(); |
| FastForwardToNextImage(); |
| image = photo_controller()->ambient_backend_model()->GetNextImage(); |
| EXPECT_FALSE(image.IsNull()); |
| |
| // Stop to refresh images. |
| photo_controller()->StopScreenUpdate(); |
| image = photo_controller()->ambient_backend_model()->GetNextImage(); |
| EXPECT_TRUE(image.IsNull()); |
| } |
| |
| // Tests that photos are updated periodically when starting screen update. |
| TEST_F(AmbientPhotoControllerTest, ShouldUpdatePhotoPeriodically) { |
| PhotoWithDetails image1; |
| PhotoWithDetails image2; |
| PhotoWithDetails image3; |
| |
| // Start to refresh images. |
| photo_controller()->StartScreenUpdate(); |
| FastForwardToNextImage(); |
| image1 = photo_controller()->ambient_backend_model()->GetNextImage(); |
| EXPECT_FALSE(image1.IsNull()); |
| EXPECT_TRUE(image2.IsNull()); |
| |
| FastForwardToNextImage(); |
| image2 = photo_controller()->ambient_backend_model()->GetNextImage(); |
| EXPECT_FALSE(image2.IsNull()); |
| EXPECT_FALSE(image1.photo.BackedBySameObjectAs(image2.photo)); |
| EXPECT_TRUE(image3.IsNull()); |
| |
| FastForwardToNextImage(); |
| image3 = photo_controller()->ambient_backend_model()->GetNextImage(); |
| 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 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 |
| // fast forward. It will also download the related images and not cache them. |
| photo_controller()->StartScreenUpdate(); |
| FastForwardToNextImage(); |
| |
| // 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 |
| // fast forward. It will also download the related images and not cache them. |
| photo_controller()->StartScreenUpdate(); |
| FastForwardToNextImage(); |
| |
| EXPECT_EQ(GetSavedCacheIndices().size(), 3u); |
| |
| auto image = photo_controller()->ambient_backend_model()->GetNextImage(); |
| EXPECT_FALSE(image.IsNull()); |
| |
| // Stop to refresh images. |
| photo_controller()->StopScreenUpdate(); |
| FastForwardToNextImage(); |
| |
| EXPECT_EQ(GetSavedCacheIndices().size(), 3u); |
| |
| image = photo_controller()->ambient_backend_model()->GetNextImage(); |
| EXPECT_TRUE(image.IsNull()); |
| } |
| |
| // Test that image is read from disk when no more topics. |
| TEST_F(AmbientPhotoControllerTest, ShouldReadCacheWhenNoMoreTopics) { |
| FetchImage(); |
| FastForwardToNextImage(); |
| // Topics is empty. Will read from cache, which is empty. |
| auto image = photo_controller()->ambient_backend_model()->GetCurrentImage(); |
| EXPECT_TRUE(image.IsNull()); |
| |
| // Save a file to check if it gets read for display. |
| std::string data("cached image"); |
| WriteCacheDataBlocking(/*cache_index=*/0, &data); |
| |
| // Reset variables in photo controller. |
| photo_controller()->StopScreenUpdate(); |
| FetchImage(); |
| FastForwardToNextImage(); |
| image = photo_controller()->ambient_backend_model()->GetCurrentImage(); |
| EXPECT_FALSE(image.IsNull()); |
| } |
| |
| // Test that will try 100 times to read image from disk when no more topics. |
| TEST_F(AmbientPhotoControllerTest, |
| ShouldTry100TimesToReadCacheWhenNoMoreTopics) { |
| FetchImage(); |
| FastForwardToNextImage(); |
| // Topics is empty. Will read from cache, which is empty. |
| auto image = photo_controller()->ambient_backend_model()->GetCurrentImage(); |
| EXPECT_TRUE(image.IsNull()); |
| |
| // The initial file name to be read is 0. Save a file with 99.img to check |
| // if it gets read for display. |
| std::string data("cached image"); |
| WriteCacheDataBlocking(/*cache_index=*/99, &data); |
| |
| // Reset variables in photo controller. |
| photo_controller()->StopScreenUpdate(); |
| FetchImage(); |
| FastForwardToNextImage(); |
| image = photo_controller()->ambient_backend_model()->GetCurrentImage(); |
| EXPECT_FALSE(image.IsNull()); |
| } |
| |
| // Test that image is read from disk when image downloading failed. |
| TEST_F(AmbientPhotoControllerTest, ShouldReadCacheWhenImageDownloadingFailed) { |
| SetDownloadPhotoData(""); |
| 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); |
| auto image = photo_controller()->ambient_backend_model()->GetCurrentImage(); |
| EXPECT_TRUE(image.IsNull()); |
| |
| // Save a file to check if it gets read for display. |
| std::string data("cached image"); |
| WriteCacheDataBlocking(/*cache_index=*/0, &data); |
| |
| // Reset variables in photo controller. |
| photo_controller()->StopScreenUpdate(); |
| FetchTopics(); |
| // Forward a little bit time. FetchTopics() will succeed. Downloading should |
| // fail. Will read from cache. |
| task_environment()->FastForwardBy(0.2 * kTopicFetchInterval); |
| image = photo_controller()->ambient_backend_model()->GetCurrentImage(); |
| EXPECT_FALSE(image.IsNull()); |
| } |
| |
| // Test that image is read from disk when image decoding failed. |
| TEST_F(AmbientPhotoControllerTest, ShouldReadCacheWhenImageDecodingFailed) { |
| SetDecodePhotoImage(gfx::ImageSkia()); |
| FetchTopics(); |
| // Forward a little bit time. FetchTopics() will succeed. |
| // Downloading succeed and save the data to disk. |
| // First decoding should fail. Will read from cache, and then succeed. |
| task_environment()->FastForwardBy(0.2 * kTopicFetchInterval); |
| auto image = photo_controller()->ambient_backend_model()->GetNextImage(); |
| EXPECT_FALSE(image.IsNull()); |
| } |
| |
| // Test that image will refresh when have more topics. |
| TEST_F(AmbientPhotoControllerTest, ShouldResumWhenHaveMoreTopics) { |
| FetchImage(); |
| FastForwardToNextImage(); |
| // Topics is empty. Will read from cache, which is empty. |
| auto image = photo_controller()->ambient_backend_model()->GetNextImage(); |
| EXPECT_TRUE(image.IsNull()); |
| |
| FetchTopics(); |
| // Forward a little bit time. FetchTopics() will succeed and refresh image. |
| task_environment()->FastForwardBy(0.2 * kTopicFetchInterval); |
| image = photo_controller()->ambient_backend_model()->GetNextImage(); |
| EXPECT_FALSE(image.IsNull()); |
| } |
| |
| TEST_F(AmbientPhotoControllerTest, ShouldDownloadBackupImagesWhenScheduled) { |
| std::string expected_data = "backup data"; |
| SetBackupDownloadPhotoData(expected_data); |
| |
| 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(); |
| EXPECT_EQ(backup_data.size(), 2u); |
| EXPECT_TRUE(base::Contains(backup_data, 0)); |
| EXPECT_TRUE(base::Contains(backup_data, 1)); |
| for (const auto& i : backup_data) { |
| EXPECT_EQ(*(i.second.image), expected_data); |
| EXPECT_FALSE(i.second.details); |
| EXPECT_FALSE(i.second.related_image); |
| } |
| } |
| |
| TEST_F(AmbientPhotoControllerTest, ShouldResetTimerWhenBackupImagesFail) { |
| ScheduleFetchBackupImages(); |
| |
| EXPECT_TRUE( |
| photo_controller()->backup_photo_refresh_timer_for_testing().IsRunning()); |
| |
| // Simulate an error in DownloadToFile. |
| ClearDownloadPhotoData(); |
| 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()); |
| |
| SetBackupDownloadPhotoData("image data"); |
| |
| 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(); |
| EXPECT_EQ(backup_data.size(), 2u); |
| EXPECT_TRUE(base::Contains(backup_data, 0)); |
| EXPECT_TRUE(base::Contains(backup_data, 1)); |
| for (const auto& i : backup_data) { |
| EXPECT_EQ(*(i.second.image), "image data"); |
| EXPECT_FALSE(i.second.details); |
| EXPECT_FALSE(i.second.related_image); |
| } |
| } |
| |
| 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. |
| SetDownloadPhotoData("image data"); |
| |
| photo_controller()->StartScreenUpdate(); |
| // Run the clock so the first photo is loaded. |
| FastForwardTiny(); |
| |
| // 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); |
| SetDownloadPhotoData("image data 2"); |
| FastForwardToNextImage(); |
| |
| // 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, ShouldStartToRefreshWeather) { |
| auto* model = photo_controller()->ambient_backend_model(); |
| EXPECT_FALSE(model->show_celsius()); |
| EXPECT_TRUE(model->weather_condition_icon().isNull()); |
| |
| WeatherInfo info; |
| info.show_celsius = true; |
| info.condition_icon_url = "https://fake-icon-url"; |
| info.temp_f = 70.0f; |
| backend_controller()->SetWeatherInfo(info); |
| |
| // Start to refresh weather as screen update starts. |
| photo_controller()->StartScreenUpdate(); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_TRUE(model->show_celsius()); |
| EXPECT_FALSE(model->weather_condition_icon().isNull()); |
| EXPECT_GT(info.temp_f, 0); |
| |
| // Refresh weather again after time passes. |
| info.show_celsius = false; |
| info.temp_f = -70.0f; |
| backend_controller()->SetWeatherInfo(info); |
| |
| FastForwardToRefreshWeather(); |
| EXPECT_FALSE(model->show_celsius()); |
| EXPECT_LT(info.temp_f, 0); |
| } |
| |
| } // namespace ash |