chromium/chromeos/ash/components/phonehub/camera_roll_thumbnail_decoder_impl_unittest.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/phonehub/camera_roll_thumbnail_decoder_impl.h"

#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#include "chromeos/ash/components/phonehub/camera_roll_item.h"
#include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"

namespace ash {
namespace phonehub {

namespace {

using BatchDecodeResult = CameraRollThumbnailDecoder::BatchDecodeResult;

void PopulateItemProto(proto::CameraRollItem* item_proto,
                       const std::string& key) {
  proto::CameraRollItemMetadata* metadata = item_proto->mutable_metadata();
  metadata->set_key(key);
  metadata->set_mime_type("image/png");
  metadata->set_last_modified_millis(123456789L);
  metadata->set_file_size_bytes(123456789L);

  proto::CameraRollItemThumbnail* thumbnail = item_proto->mutable_thumbnail();
  thumbnail->set_format(proto::CameraRollItemThumbnail_Format_JPEG);
  thumbnail->set_data("encoded_thumbnail_data");
}

const CameraRollItem CreateItem(const std::string& key) {
  proto::CameraRollItemMetadata metadata;
  metadata.set_key(key);
  metadata.set_mime_type("image/png");
  metadata.set_last_modified_millis(123456789L);
  metadata.set_file_size_bytes(123456789L);

  SkBitmap test_bitmap;
  test_bitmap.allocN32Pixels(1, 1);
  gfx::ImageSkia image_skia = gfx::ImageSkia::CreateFrom1xBitmap(test_bitmap);
  image_skia.MakeThreadSafe();
  gfx::Image thumbnail(image_skia);

  return CameraRollItem(metadata, thumbnail);
}

// Verifies that the metadata of a generated item matches the corresponding
// proto input.
void VerifyMetadataEqual(const proto::CameraRollItemMetadata& expected,
                         const proto::CameraRollItemMetadata& actual) {
  EXPECT_EQ(expected.key(), actual.key());
  EXPECT_EQ(expected.mime_type(), actual.mime_type());
  EXPECT_EQ(expected.last_modified_millis(), actual.last_modified_millis());
  EXPECT_EQ(expected.file_size_bytes(), actual.file_size_bytes());
}

}  // namespace

class FakeDecoderDelegate
    : public CameraRollThumbnailDecoderImpl::DecoderDelegate {
 public:
  FakeDecoderDelegate() = default;
  ~FakeDecoderDelegate() override = default;

  void DecodeThumbnail(
      const CameraRollThumbnailDecoderImpl::DecodeRequest& request,
      data_decoder::DecodeImageCallback callback) override {
    pending_callbacks_[request.GetMetadata().key()] = std::move(callback);
  }

  void CompleteRequest(const std::string& key) {
    SkBitmap test_bitmap;
    test_bitmap.allocN32Pixels(1, 1);
    std::move(pending_callbacks_.at(key)).Run(test_bitmap);
    pending_callbacks_.erase(key);
  }

  void FailRequest(const std::string& key) {
    SkBitmap test_bitmap;
    std::move(pending_callbacks_.at(key)).Run(test_bitmap);
    pending_callbacks_.erase(key);
  }

  void CompleteAllRequests() {
    SkBitmap test_bitmap;
    test_bitmap.allocN32Pixels(1, 1);
    for (auto& it : pending_callbacks_)
      std::move(it.second).Run(test_bitmap);
    pending_callbacks_.clear();
  }

 private:
  base::flat_map<std::string, data_decoder::DecodeImageCallback>
      pending_callbacks_;
};

class CameraRollThumbnailDecoderImplTest : public testing::Test {
 protected:
  CameraRollThumbnailDecoderImplTest() = default;
  CameraRollThumbnailDecoderImplTest(
      const CameraRollThumbnailDecoderImplTest&) = delete;
  CameraRollThumbnailDecoderImplTest& operator=(
      const CameraRollThumbnailDecoderImplTest&) = delete;
  ~CameraRollThumbnailDecoderImplTest() override = default;

  void SetUp() override {
    decoder_.decoder_delegate_ = std::make_unique<FakeDecoderDelegate>();
  }

  void BatchDecode(const proto::FetchCameraRollItemsResponse& response,
                   const std::vector<CameraRollItem>& current_items) {
    decoder_.BatchDecode(
        response, current_items,
        base::BindOnce(&CameraRollThumbnailDecoderImplTest::OnItemsDecoded,
                       weak_ptr_factory_.GetWeakPtr()));
  }

  void CompleteDecodeRequest(const std::string& key) {
    static_cast<FakeDecoderDelegate*>(decoder_.decoder_delegate_.get())
        ->CompleteRequest(key);
  }

  void FailDecodeRequest(const std::string& key) {
    static_cast<FakeDecoderDelegate*>(decoder_.decoder_delegate_.get())
        ->FailRequest(key);
  }

  void CompleteAllDecodeRequests() {
    static_cast<FakeDecoderDelegate*>(decoder_.decoder_delegate_.get())
        ->CompleteAllRequests();
  }

  int completed_batch_count() const { return completed_batch_count_; }

  BatchDecodeResult last_decode_result() const { return last_decode_result_; }

  // Verifies decoded items match the list of items in the provided
  // |FetchCameraRollItemsResponse|, and their thumbnails have been properly
  // decoded.
  void VerifyDecodedItemsMatchResponse(
      const proto::FetchCameraRollItemsResponse& response) const {
    int decoded_item_count = last_decoded_items_.size();
    EXPECT_EQ(response.items_size(), decoded_item_count);
    for (int i = 0; i < decoded_item_count; i++) {
      VerifyDecodedItem(/*decoded_item_index=*/i, response.items(i));
    }
  }

  void VerifyDecodedItem(int decoded_item_index,
                         const proto::CameraRollItem& input_item) const {
    const CameraRollItem& decoded_item =
        last_decoded_items_.at(decoded_item_index);
    VerifyMetadataEqual(input_item.metadata(), decoded_item.metadata());
    EXPECT_FALSE(decoded_item.thumbnail().IsEmpty());
  }

  void OnItemsDecoded(BatchDecodeResult result,
                      const std::vector<CameraRollItem>& items) {
    completed_batch_count_++;
    last_decode_result_ = result;
    last_decoded_items_ = items;
  }

 private:
  CameraRollThumbnailDecoderImpl decoder_;
  int completed_batch_count_ = 0;
  BatchDecodeResult last_decode_result_;
  std::vector<CameraRollItem> last_decoded_items_;
  base::WeakPtrFactory<CameraRollThumbnailDecoderImplTest> weak_ptr_factory_{
      this};
};

TEST_F(CameraRollThumbnailDecoderImplTest, BatchDecodeWithNoExistingItems) {
  proto::FetchCameraRollItemsResponse response;
  PopulateItemProto(response.add_items(), "key3");
  PopulateItemProto(response.add_items(), "key2");
  PopulateItemProto(response.add_items(), "key1");

  BatchDecode(response, {});

  CompleteDecodeRequest("key3");
  CompleteDecodeRequest("key2");
  EXPECT_EQ(0, completed_batch_count());

  // The batch of items won't be added until decode request are completed for
  // all items.
  CompleteDecodeRequest("key1");
  EXPECT_EQ(1, completed_batch_count());
  EXPECT_EQ(BatchDecodeResult::kCompleted, last_decode_result());
  VerifyDecodedItemsMatchResponse(response);
}

TEST_F(CameraRollThumbnailDecoderImplTest,
       BatchDecodeWithOutOfOrderCompletions) {
  proto::FetchCameraRollItemsResponse response;
  PopulateItemProto(response.add_items(), "key3");
  PopulateItemProto(response.add_items(), "key2");
  PopulateItemProto(response.add_items(), "key1");

  BatchDecode(response, {});

  CompleteDecodeRequest("key2");
  CompleteDecodeRequest("key1");
  CompleteDecodeRequest("key3");
  EXPECT_EQ(1, completed_batch_count());
  EXPECT_EQ(BatchDecodeResult::kCompleted, last_decode_result());
  // Verify that the order in which the thumbnails are decoded does not affect
  // the final display order of the items.
  VerifyDecodedItemsMatchResponse(response);
}

TEST_F(CameraRollThumbnailDecoderImplTest, BatchDecodeWithErrors) {
  proto::FetchCameraRollItemsResponse response;
  PopulateItemProto(response.add_items(), "key3");
  PopulateItemProto(response.add_items(), "key2");
  PopulateItemProto(response.add_items(), "key1");

  BatchDecode(response, {});

  CompleteDecodeRequest("key3");
  CompleteDecodeRequest("key1");
  FailDecodeRequest("key2");
  EXPECT_EQ(1, completed_batch_count());
  EXPECT_EQ(BatchDecodeResult::kCompleted, last_decode_result());
  VerifyDecodedItem(/*decoded_item_index=*/0, response.items(0));
  VerifyDecodedItem(/*decoded_item_index=*/1, response.items(2));
}

TEST_F(CameraRollThumbnailDecoderImplTest,
       BatchDecodeWithMissingThumbnailData) {
  std::vector<CameraRollItem> existing_items;
  existing_items.push_back(CreateItem("key2"));
  existing_items.push_back(CreateItem("key1"));
  proto::FetchCameraRollItemsResponse response;
  // This is a new item but its thumbnail data is missing.
  PopulateItemProto(response.add_items(), "key3");
  response.mutable_items(0)->clear_thumbnail();
  PopulateItemProto(response.add_items(), "key2");

  BatchDecode(response, existing_items);
  CompleteAllDecodeRequests();

  EXPECT_EQ(1, completed_batch_count());
  EXPECT_EQ(BatchDecodeResult::kCancelled, last_decode_result());
}

TEST_F(CameraRollThumbnailDecoderImplTest, BatchDecodeWithExistingItems) {
  std::vector<CameraRollItem> existing_items;
  existing_items.push_back(CreateItem("key3"));
  existing_items.push_back(CreateItem("key2"));
  existing_items.push_back(CreateItem("key1"));
  proto::FetchCameraRollItemsResponse response;
  PopulateItemProto(response.add_items(), "key4");
  // Thumbnails won't be sent with the proto if an item's data is already
  // available and up-to-date.
  PopulateItemProto(response.add_items(), "key3");
  response.mutable_items(1)->clear_thumbnail();
  PopulateItemProto(response.add_items(), "key2");
  response.mutable_items(2)->clear_thumbnail();

  BatchDecode(response, existing_items);
  EXPECT_EQ(0, completed_batch_count());

  // Only need to decode thumbnail of this new item.
  CompleteDecodeRequest("key4");
  EXPECT_EQ(1, completed_batch_count());
  EXPECT_EQ(BatchDecodeResult::kCompleted, last_decode_result());
  VerifyDecodedItemsMatchResponse(response);
}

TEST_F(CameraRollThumbnailDecoderImplTest,
       BatchDecodeWithExistingItemsInDifferentOrder) {
  std::vector<CameraRollItem> existing_items;
  existing_items.push_back(CreateItem("key3"));
  existing_items.push_back(CreateItem("key2"));
  existing_items.push_back(CreateItem("key1"));
  proto::FetchCameraRollItemsResponse response;
  PopulateItemProto(response.add_items(), "key2");
  response.mutable_items(0)->clear_thumbnail();
  PopulateItemProto(response.add_items(), "key3");
  response.mutable_items(1)->clear_thumbnail();
  PopulateItemProto(response.add_items(), "key1");
  response.mutable_items(2)->clear_thumbnail();

  BatchDecode(response, existing_items);

  // No additional thumbnail decoding is needed because the items are already
  // available.
  EXPECT_EQ(1, completed_batch_count());
  EXPECT_EQ(BatchDecodeResult::kCompleted, last_decode_result());
  VerifyDecodedItemsMatchResponse(response);
}

TEST_F(CameraRollThumbnailDecoderImplTest, BatchDecodeWithInProgresRequests) {
  proto::FetchCameraRollItemsResponse first_response;
  PopulateItemProto(first_response.add_items(), "key2");
  PopulateItemProto(first_response.add_items(), "key1");

  BatchDecode(first_response, {});
  CompleteDecodeRequest("key1");

  proto::FetchCameraRollItemsResponse second_response;
  PopulateItemProto(second_response.add_items(), "key3");
  PopulateItemProto(second_response.add_items(), "key2");
  BatchDecode(second_response, {});
  // The first in-progress request should be cancelled
  EXPECT_EQ(1, completed_batch_count());
  EXPECT_EQ(BatchDecodeResult::kCancelled, last_decode_result());

  CompleteAllDecodeRequests();
  EXPECT_EQ(2, completed_batch_count());
  EXPECT_EQ(BatchDecodeResult::kCompleted, last_decode_result());
  VerifyDecodedItemsMatchResponse(second_response);
}

}  // namespace phonehub
}  // namespace ash