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
|
// 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
|