1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167

ash / ambient / model / ambient_animation_attribution_provider.cc [blame]

// Copyright 2022 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/model/ambient_animation_attribution_provider.h"

#include <optional>
#include <string>

#include "ash/ambient/model/ambient_backend_model.h"
#include "ash/public/cpp/ambient/proto/photo_cache_entry.pb.h"
#include "ash/utility/lottie_util.h"
#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "cc/paint/skottie_wrapper.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/lottie/animation.h"

namespace ash {

namespace {

// Returns the attribution text node name with the given |idx|.
// 1 -> "CrOS_AttributionNode1"
// 2 -> "CrOS_AttributionNode2"
// ...
std::string BuildAttributionNodeName(int idx) {
  return base::StrCat({kLottieCustomizableIdPrefix, "_Attribution_Text",
                       base::NumberToString(idx)});
}

// Not all text nodes in the animation are necessarily ones that should hold
// photo attribution. Some animations may have static text embedded in them.
// Filter these out.
//
// The returned vector is intentionally sorted by the corresponding node name
// in string format:
// {
//   hash("CrOS_AttributionNode1"),
//   hash("CrOS_AttributionNode2"),
//   ...,
//   hash("CrOS_AttributionNodeN")
// }
// See "UX Guidance" below for why.
std::vector<cc::SkottieResourceIdHash> GetAttributionNodeIds(
    const cc::SkottieWrapper& skottie) {
  RE2 attribution_node_pattern(base::StrCat(
      {kLottieCustomizableIdPrefix, R"(_Attribution_Text([[:digit:]]+))"}));

  // Note the indices are not required to be contiguous (1, 2, 3, ...). In
  // practice they probably are, but the code can handle "gaps" (1, 2, 4, ...).
  base::flat_set<int> attribution_node_indices_sorted;
  for (const std::string& text_node_name : skottie.GetTextNodeNames()) {
    if (!IsCustomizableLottieId(text_node_name)) {
      DVLOG(4) << "Ignoring static text node in animation";
      continue;
    }

    // Index embedded within the attribution text node's name:
    // "CrOS_AttributionNode1" -> 1
    // "CrOS_AttributionNode2" -> 2
    int attribution_node_idx = 0;
    if (!RE2::FullMatch(text_node_name, attribution_node_pattern,
                        &attribution_node_idx)) {
      LOG(DFATAL) << "Failed to parse index from text attribution node "
                  << text_node_name;
      continue;
    }

    if (!attribution_node_indices_sorted.insert(attribution_node_idx).second) {
      LOG(DFATAL) << "Found duplicated attribution node names: "
                  << text_node_name;
    }
  }
  std::vector<cc::SkottieResourceIdHash> attribution_node_ids;
  for (int idx : attribution_node_indices_sorted) {
    attribution_node_ids.push_back(
        cc::HashSkottieResourceId(BuildAttributionNodeName(idx)));
  }
  return attribution_node_ids;
}

}  // namespace

AmbientAnimationAttributionProvider::AmbientAnimationAttributionProvider(
    AmbientAnimationPhotoProvider* photo_provider,
    lottie::Animation* animation)
    : animation_(animation),
      attribution_node_ids_(GetAttributionNodeIds(*animation_->skottie())) {
  DCHECK(animation_);
  observation_.Observe(photo_provider);
}

AmbientAnimationAttributionProvider::~AmbientAnimationAttributionProvider() =
    default;

// UX Guidance:
// 1) The dynamic image assets and attribution text nodes in an animation are
//    identified by 2 different sets of strings. Ex:
//    Dynamic image asset ids:
//    * "_CrOS_Photo_PositionA_1"
//    * "_CrOS_Photo_PositionB_1"
//    ...
//    * "_CrOS_Photo_Position<P>_N"
//    Attribution text node names:
//    * "_CrOS_AttributionText1"
//    * "_CrOS_AttributionText2"
//    ...
//    * "_CrOS_AttributionTextN"
//
//    To assign an image asset to an attribution text node, sort the
//    asset ids by index first and position second (this is taken care of
//    already by ParsedDynamicAssetId's comparison operator and the use of
//    a sorted base::flat_map for |new_topics| below):
//    1) "_CrOS_Photo_PositionA_1" (Index 1 Position A)
//    2) "_CrOS_Photo_PositionB_1" (Index 1 Position B)
//    3) "_CrOS_Photo_PositionA_2" (Index 2 Position A)
//    4) "_CrOS_Photo_PositionB_2" (Index 2 Position B)
//
//    And sort the attribution text nodes by their name:
//    1) "_CrOS_AttributionText1"
//    2) "_CrOS_AttributionText2"
//    3) "_CrOS_AttributionText3"
//    4) "_CrOS_AttributionText4"
//
//    Afterwards, assign sorted asset <i> to sorted attribution node <i>. Note
//    the animation is allowed to have fewer attribution nodes than dynamic
//    image assets. In this case, the dynamic image assets left without a
//    corresponding attribution text node are just ignored by design.
//
// 2) If a photo has no attribution (an empty string), just set its
//    corresponding text node to be blank (an empty string). This is a
//    corner case though. In practice, either all of the photos in the set
//    should have an associated attribution, or none do.
void AmbientAnimationAttributionProvider::OnDynamicImageAssetsRefreshed(
    const base::flat_map<ambient::util::ParsedDynamicAssetId,
                         std::reference_wrapper<const PhotoWithDetails>>&
        new_topics) {
  DCHECK_GE(new_topics.size(), attribution_node_ids_.size())
      << "All ambient-mode animations should have at least as many dynamic "
         "image assets as text attribution nodes.";
  auto new_topics_iter = new_topics.begin();
  auto attribution_node_ids_iter = attribution_node_ids_.begin();
  for (; attribution_node_ids_iter != attribution_node_ids_.end();
       ++new_topics_iter, ++attribution_node_ids_iter) {
    cc::SkottieResourceIdHash attribution_node_id = *attribution_node_ids_iter;
    DCHECK(animation_->text_map().contains(attribution_node_id));
    const PhotoWithDetails& new_topic = new_topics_iter->second.get();
    if (new_topic.topic_type == ::ambient::kPersonal) {
      // Per UX: Don't display attribution text (which is just the album name)
      // for personal photos in animations.
      //
      // The attribution node's previous contents (if any) still need to be
      // cleared though.
      animation_->text_map().at(attribution_node_id).SetText("");
    } else {
      animation_->text_map().at(attribution_node_id).SetText(new_topic.details);
    }
  }
}

}  // namespace ash