// Copyright 2023 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_managed_photo_controller.h"
#include <memory>
#include "ash/ambient/metrics/managed_screensaver_metrics.h"
#include "ash/ambient/model/ambient_backend_model.h"
#include "ash/ambient/model/ambient_photo_config.h"
#include "ash/ambient/model/ambient_slideshow_photo_config.h"
#include "ash/ambient/test/ambient_ash_test_base.h"
#include "ash/ambient/test/mock_ambient_backend_model_observer.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_paths.h"
#include "ash/public/cpp/ambient/proto/photo_cache_entry.pb.h"
#include "ash/public/cpp/test/in_process_data_decoder.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_path_override.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_unittest_util.h"
namespace ash {
using gfx::test::AreImagesEqual;
using ::testing::IsEmpty;
using ::testing::Not;
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_P(BackedBySameImageAs, photo_with_details, "") {
return AreBackedBySameImage(arg, photo_with_details);
}
// This limit is specified in the policy definition for the policies
// ScreensaverLockScreenImages and DeviceScreensaverLoginScreenImages.
constexpr size_t kMaxUrlsToProcessFromPolicy = 25u;
} // namespace
class AmbientManagedPhotoControllerTest : public AmbientAshTestBase {
public:
AmbientManagedPhotoControllerTest() {
CreateTestData();
// Required as otherwise the PathService::CheckedGet fails in the
// screensaver images policy handler.
device_policy_screensaver_folder_override_ =
std::make_unique<base::ScopedPathOverride>(
ash::DIR_DEVICE_POLICY_SCREENSAVER_DATA, temp_dir_.GetPath());
}
void CreateTestData() {
bool success = temp_dir_.CreateUniqueTempDir();
ASSERT_TRUE(success);
base::FilePath image_1 =
temp_dir_.GetPath().Append(FILE_PATH_LITERAL("IMAGE_1.jpg"));
CreateTestImageJpegFile(image_1, 4, 4, SK_ColorRED);
base::FilePath image_2 =
temp_dir_.GetPath().Append(FILE_PATH_LITERAL("IMAGE_2.jpg"));
CreateTestImageJpegFile(image_2, 8, 8, SK_ColorGREEN);
base::FilePath image_3 =
temp_dir_.GetPath().Append(FILE_PATH_LITERAL("IMAGE_3.jpg"));
CreateTestImageJpegFile(image_3, 8, 4, SK_ColorBLUE);
base::FilePath image_4 =
temp_dir_.GetPath().Append(FILE_PATH_LITERAL("IMAGE_4.jpg"));
CreateTestImageJpegFile(image_4, 4, 8, SK_ColorBLACK);
image_file_paths_.push_back(image_1);
image_file_paths_.push_back(image_2);
image_file_paths_.push_back(image_3);
image_file_paths_.push_back(image_4);
}
void CleanUpTestData() { image_file_paths_.clear(); }
void SetUp() override {
scoped_feature_list_.InitAndEnableFeature(
ash::features::kAmbientModeManagedScreensaver);
AmbientAshTestBase::SetUp();
photo_controller_ = std::make_unique<AmbientManagedPhotoController>(
*ambient_controller()->ambient_view_delegate(),
CreateAmbientManagedSlideshowPhotoConfig());
}
void TearDown() override {
StopScreenUpdate();
// Call reset before calling tear down to make sure we aren't observing
// already freed resources
photo_controller_.reset();
AmbientAshTestBase::TearDown();
CleanUpTestData();
}
std::vector<base::FilePath> GetImageFilePaths() { return image_file_paths_; }
void RunUntilImagesReady() {
if (managed_photo_controller()->ambient_backend_model()->ImagesReady()) {
return;
}
base::test::TestFuture<void> future;
testing::NiceMock<MockAmbientBackendModelObserver> mock_backend_observer;
base::ScopedObservation<AmbientBackendModel, AmbientBackendModelObserver>
scoped_observation{&mock_backend_observer};
scoped_observation.Observe(
managed_photo_controller()->ambient_backend_model());
ON_CALL(mock_backend_observer, OnImagesReady)
.WillByDefault(::testing::Invoke([&future]() { future.SetValue(); }));
ASSERT_TRUE(future.Wait()) << "Timed out waiting for OnImagesReady";
}
void RunUntilNextImagesAdded(size_t expected_topics) {
size_t num_topics_added = 0;
base::test::TestFuture<void> future;
testing::NiceMock<MockAmbientBackendModelObserver> mock_backend_observer;
base::ScopedObservation<AmbientBackendModel, AmbientBackendModelObserver>
scoped_observation{&mock_backend_observer};
scoped_observation.Observe(
managed_photo_controller()->ambient_backend_model());
ON_CALL(mock_backend_observer, OnImageAdded)
.WillByDefault(
::testing::Invoke([&future, &num_topics_added, &expected_topics]() {
num_topics_added++;
if (expected_topics == num_topics_added) {
future.SetValue();
}
}));
ASSERT_TRUE(future.Wait()) << "Timed out waiting for OnImageAdded";
}
void StopScreenUpdate() {
managed_photo_controller()->StopScreenUpdate();
EXPECT_FALSE(managed_photo_controller()->IsScreenUpdateActive());
EXPECT_THAT(managed_photo_controller()
->ambient_backend_model()
->all_decoded_topics(),
IsEmpty());
}
void StartScreenUpdate() {
managed_photo_controller()->StartScreenUpdate();
EXPECT_TRUE(managed_photo_controller()->IsScreenUpdateActive());
}
AmbientManagedPhotoController* managed_photo_controller() {
return photo_controller_.get();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
InProcessDataDecoder decoder_;
std::vector<base::FilePath> image_file_paths_;
base::ScopedTempDir temp_dir_;
std::unique_ptr<base::ScopedPathOverride>
device_policy_screensaver_folder_override_;
std::unique_ptr<AmbientManagedPhotoController> photo_controller_;
};
TEST_F(AmbientManagedPhotoControllerTest,
NoImagesAreShownIfThereAreNoImagesAvailable) {
StartScreenUpdate();
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
IsEmpty());
task_environment()->FastForwardBy(base::Minutes(1));
EXPECT_FALSE(
managed_photo_controller()->ambient_backend_model()->ImagesReady());
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
IsEmpty());
}
TEST_F(AmbientManagedPhotoControllerTest,
WhenImagesAreSetBackendModelHasImages) {
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
IsEmpty());
managed_photo_controller()->UpdateImageFilePaths(GetImageFilePaths());
StartScreenUpdate();
RunUntilImagesReady();
EXPECT_TRUE(
managed_photo_controller()->ambient_backend_model()->ImagesReady());
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
Not(IsEmpty()));
}
TEST_F(AmbientManagedPhotoControllerTest,
WhenImagesAreSetAfterStartingUpdatedBackendModelHasImages) {
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
IsEmpty());
StartScreenUpdate();
EXPECT_FALSE(
managed_photo_controller()->ambient_backend_model()->ImagesReady());
managed_photo_controller()->UpdateImageFilePaths(GetImageFilePaths());
RunUntilImagesReady();
EXPECT_TRUE(
managed_photo_controller()->ambient_backend_model()->ImagesReady());
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
Not(IsEmpty()));
}
TEST_F(AmbientManagedPhotoControllerTest,
UICycleEndMarkerTransitionsToTheNextImage) {
managed_photo_controller()->UpdateImageFilePaths(GetImageFilePaths());
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
IsEmpty());
StartScreenUpdate();
RunUntilImagesReady();
PhotoWithDetails next_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
nullptr, &next_image);
managed_photo_controller()->OnMarkerHit(
AmbientPhotoConfig::Marker::kUiCycleEnded);
RunUntilNextImagesAdded(/*expected_topics=*/1);
PhotoWithDetails current_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
¤t_image, nullptr);
EXPECT_THAT(current_image, BackedBySameImageAs(next_image));
}
TEST_F(AmbientManagedPhotoControllerTest,
UIStartRenderingMarkerDoesNotTransitionImages) {
managed_photo_controller()->UpdateImageFilePaths(GetImageFilePaths());
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
IsEmpty());
StartScreenUpdate();
RunUntilImagesReady();
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
Not(IsEmpty()));
PhotoWithDetails old_current_image, next_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
&old_current_image, &next_image);
managed_photo_controller()->OnMarkerHit(
AmbientPhotoConfig::Marker::kUiStartRendering);
PhotoWithDetails current_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
¤t_image, nullptr);
EXPECT_THAT(current_image, Not(BackedBySameImageAs(next_image)));
EXPECT_THAT(current_image, BackedBySameImageAs(old_current_image));
}
TEST_F(AmbientManagedPhotoControllerTest, FirstImageIsLoadedAfterLastImage) {
const std::vector<base::FilePath>& image_file_paths = GetImageFilePaths();
managed_photo_controller()->UpdateImageFilePaths(
{image_file_paths[0], image_file_paths[1]});
StartScreenUpdate();
RunUntilImagesReady();
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
Not(IsEmpty()));
PhotoWithDetails first_image, second_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
&first_image, &second_image);
EXPECT_FALSE(AreImagesEqual(gfx::Image(first_image.photo),
gfx::Image(second_image.photo)));
managed_photo_controller()->OnMarkerHit(
AmbientPhotoConfig::Marker::kUiCycleEnded);
RunUntilNextImagesAdded(/*expected_topics=*/1);
PhotoWithDetails third_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
nullptr, &third_image);
// The third image will either be the 2nd or the 1st image in the backend
// model the reason for this is that the order isn't guaranteed when we load
// the first 2 images in parallel (so it can either be [1,2] or [2,1]).
EXPECT_TRUE(AreImagesEqual(gfx::Image(first_image.photo),
gfx::Image(third_image.photo)) ||
AreImagesEqual(gfx::Image(second_image.photo),
gfx::Image(third_image.photo)));
}
TEST_F(AmbientManagedPhotoControllerTest,
UpdatingImagesDuringDisplayUpdatesThem) {
const std::vector<base::FilePath>& image_file_paths = GetImageFilePaths();
managed_photo_controller()->UpdateImageFilePaths(
{image_file_paths[0], image_file_paths[1]});
StartScreenUpdate();
RunUntilImagesReady();
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
Not(IsEmpty()));
PhotoWithDetails first_image, second_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
&first_image, &second_image);
managed_photo_controller()->UpdateImageFilePaths(
{image_file_paths[2], image_file_paths[3]});
// Wait for the next images
RunUntilNextImagesAdded(/*expected_topics=*/2);
PhotoWithDetails third_image, fourth_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
&third_image, &fourth_image);
// The new images are different from the old images.
EXPECT_FALSE(AreImagesEqual(gfx::Image(first_image.photo),
gfx::Image(third_image.photo)));
EXPECT_FALSE(AreImagesEqual(gfx::Image(first_image.photo),
gfx::Image(fourth_image.photo)));
EXPECT_FALSE(AreImagesEqual(gfx::Image(second_image.photo),
gfx::Image(third_image.photo)));
EXPECT_FALSE(AreImagesEqual(gfx::Image(second_image.photo),
gfx::Image(fourth_image.photo)));
}
TEST_F(AmbientManagedPhotoControllerTest, CallingStartScreenAgainIsANoOp) {
managed_photo_controller()->UpdateImageFilePaths(GetImageFilePaths());
StartScreenUpdate();
RunUntilImagesReady();
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
Not(IsEmpty()));
PhotoWithDetails first_image, second_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
&first_image, &second_image);
managed_photo_controller()->StartScreenUpdate();
task_environment()->FastForwardBy(base::Minutes(1));
PhotoWithDetails after_update_first_image, after_update_second_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
&after_update_first_image, &after_update_second_image);
// Note: No change happens, and we still have the same image instances at the
// same positions in the buffer.
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
Not(IsEmpty()));
EXPECT_THAT(first_image, BackedBySameImageAs(after_update_first_image));
EXPECT_THAT(second_image, BackedBySameImageAs(after_update_second_image));
}
TEST_F(AmbientManagedPhotoControllerTest, InvalidFileTest) {
managed_photo_controller()->UpdateImageFilePaths(
{base::FilePath(FILE_PATH_LITERAL("invalid_path_1")),
base::FilePath(FILE_PATH_LITERAL("invalid_path_2"))});
StartScreenUpdate();
task_environment()->FastForwardBy(base::Minutes(1));
EXPECT_THAT(
managed_photo_controller()->ambient_backend_model()->all_decoded_topics(),
IsEmpty());
}
TEST_F(AmbientManagedPhotoControllerTest, ValidFileNotLoadedTwice) {
const std::vector<base::FilePath>& image_file_paths = GetImageFilePaths();
managed_photo_controller()->UpdateImageFilePaths({
base::FilePath(FILE_PATH_LITERAL("invalid_path_1")),
image_file_paths[0],
base::FilePath(FILE_PATH_LITERAL("invalid_path_2")),
});
StartScreenUpdate();
RunUntilNextImagesAdded(/*expected_topics=*/1);
task_environment()->FastForwardBy(base::Minutes(1));
EXPECT_EQ(managed_photo_controller()
->ambient_backend_model()
->all_decoded_topics()
.size(),
1u);
// Case: Marker hit when max tries exceeded.
managed_photo_controller()->OnMarkerHit(
AmbientPhotoConfig::Marker::kUiCycleEnded);
task_environment()->FastForwardBy(base::Minutes(1));
EXPECT_EQ(managed_photo_controller()
->ambient_backend_model()
->all_decoded_topics()
.size(),
1u);
// Case: Updating image file paths resets retry limit
managed_photo_controller()->UpdateImageFilePaths(
{image_file_paths[0], image_file_paths[1]});
RunUntilNextImagesAdded(/*expected_topics=*/2);
EXPECT_EQ(managed_photo_controller()
->ambient_backend_model()
->all_decoded_topics()
.size(),
2u);
}
TEST_F(AmbientManagedPhotoControllerTest, InvalidAndValidFileTest) {
const std::vector<base::FilePath>& image_file_paths = GetImageFilePaths();
managed_photo_controller()->UpdateImageFilePaths(
{image_file_paths[0], base::FilePath(FILE_PATH_LITERAL("invalid_path_1")),
base::FilePath(FILE_PATH_LITERAL("invalid_path_2")),
base::FilePath(FILE_PATH_LITERAL("invalid_path_3")),
image_file_paths[1]});
StartScreenUpdate();
RunUntilImagesReady();
EXPECT_EQ(managed_photo_controller()
->ambient_backend_model()
->all_decoded_topics()
.size(),
2u);
PhotoWithDetails first_image, second_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
&first_image, &second_image);
EXPECT_FALSE(AreImagesEqual(gfx::Image(first_image.photo),
gfx::Image(second_image.photo)));
// Case: Marker hit in a mix of valid and invalid files.
managed_photo_controller()->OnMarkerHit(
AmbientPhotoConfig::Marker::kUiCycleEnded);
RunUntilNextImagesAdded(/*expected_topics=*/1);
PhotoWithDetails third_image, fourth_image;
managed_photo_controller()->ambient_backend_model()->GetCurrentAndNextImages(
&third_image, &fourth_image);
EXPECT_FALSE(AreImagesEqual(gfx::Image(third_image.photo),
gfx::Image(fourth_image.photo)));
}
TEST_F(AmbientManagedPhotoControllerTest, PhotoConfigTest) {
const AmbientPhotoConfig& config =
managed_photo_controller()->ambient_backend_model()->photo_config();
EXPECT_EQ(2u, config.GetNumDecodedTopicsToBuffer());
EXPECT_TRUE(config.should_split_topics);
EXPECT_EQ(1u, config.refresh_topic_markers.size());
EXPECT_TRUE(config.refresh_topic_markers.contains(
AmbientPhotoConfig::Marker::kUiCycleEnded));
}
TEST_F(AmbientManagedPhotoControllerTest, AddingEmptyImagesIsANoOP) {
managed_photo_controller()->UpdateImageFilePaths(GetImageFilePaths());
StartScreenUpdate();
RunUntilImagesReady();
managed_photo_controller()->UpdateImageFilePaths({});
task_environment()->FastForwardBy(base::Minutes(1));
EXPECT_EQ(managed_photo_controller()
->ambient_backend_model()
->all_decoded_topics()
.size(),
2u);
}
TEST_F(AmbientManagedPhotoControllerTest, VerifyImageCountHistogram) {
base::HistogramTester histogram_tester;
std::vector<base::FilePath> images;
// Update image list to empty list
managed_photo_controller()->UpdateImageFilePaths(images);
const std::string& histogram_name =
GetManagedScreensaverHistogram(kManagedScreensaverImageCountUMA);
// Update list to max - 1
for (unsigned int i = 0; i < kMaxUrlsToProcessFromPolicy - 1; ++i) {
images.emplace_back(FILE_PATH_LITERAL("IMAGE_1.jpg"));
}
managed_photo_controller()->UpdateImageFilePaths(images);
// Update list to max
images.emplace_back(FILE_PATH_LITERAL("IMAGE_1.jpg"));
managed_photo_controller()->UpdateImageFilePaths(images);
// Update list to max + 1
images.emplace_back(FILE_PATH_LITERAL("IMAGE_1.jpg"));
managed_photo_controller()->UpdateImageFilePaths(images);
histogram_tester.ExpectTotalCount(histogram_name, 4);
histogram_tester.ExpectBucketCount(histogram_name,
/*sample=*/0, /*expected_count=*/1);
histogram_tester.ExpectBucketCount(histogram_name,
/*sample=*/kMaxUrlsToProcessFromPolicy - 1,
/*expected_count=*/1);
histogram_tester.ExpectBucketCount(histogram_name,
/*sample=*/kMaxUrlsToProcessFromPolicy + 1,
/*expected_count=*/1);
histogram_tester.ExpectBucketCount(histogram_name,
/*sample=*/kMaxUrlsToProcessFromPolicy + 1,
/*expected_count=*/1);
}
} // namespace ash