/* * Copyright (c) 2021-2022 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include "ui_model.h" namespace OHOS::uitest { using namespace std; using namespace nlohmann; static constexpr auto ROOT_HIERARCHY = "ROOT"; static string Rect2JsonStr(const Rect &rect) { json data; data["left"] = rect.left_; data["top"] = rect.top_; data["right"] = rect.right_; data["bottom"] = rect.bottom_; return data.dump(); } bool Widget::HasAttr(string_view name) const { return attributes_.find(string(name)) != attributes_.end(); } string Widget::GetAttr(string_view name, string_view defaultVal) const { auto find = attributes_.find(string(name)); return find == attributes_.end() ? string(defaultVal) : find->second; } void Widget::SetAttr(string_view name, string_view value) { attributes_[string(name)] = value; } void Widget::SetHostTreeId(string_view tid) { hostTreeId_ = tid; } string Widget::GetHostTreeId() const { return hostTreeId_; } void Widget::SetBounds(const Rect &bounds) { bounds_ = bounds; // save bounds attribute as structured data SetAttr(ATTR_NAMES[UiAttr::BOUNDS], Rect2JsonStr(bounds_)); } string Widget::ToStr() const { stringstream os; os << "Widget{"; for (auto &pair : attributes_) { os << pair.first << "='" << pair.second << "',"; } os << "}"; return os.str(); } unique_ptr Widget::Clone(string_view hostTreeId, string_view hierarchy) const { auto clone = make_unique(hierarchy); clone->hostTreeId_ = hostTreeId; clone->attributes_ = this->attributes_; clone->bounds_ = this->bounds_; clone->SetAttr(ATTR_NAMES[UiAttr::HIERARCHY], hierarchy); // ensure hiararchy consisent return clone; } std::map Widget::GetAttrMap() const { return attributes_; } class WidgetHierarchyBuilder { public: static string Build(string_view parentWidgetHierarchy, uint32_t childIndex) { return string(parentWidgetHierarchy) + string(hierarchySeparator_) + to_string(childIndex); } static string GetParentWidgetHierarchy(string_view hierarchy) { if (hierarchy == ROOT_HIERARCHY) { // no parent for root widget return ""; } auto findRoot = hierarchy.find(ROOT_HIERARCHY); if (findRoot != 0) { // invalid hierarchy string return ""; } auto findLastSeparator = hierarchy.find_last_of(hierarchySeparator_); if (findLastSeparator <= 0 || findLastSeparator == string::npos) { return ""; } return string(hierarchy).substr(0, findLastSeparator); } static string GetChildHierarchy(string_view hierarchy, uint32_t childIndex) { if (hierarchy.find(ROOT_HIERARCHY) != 0) { // invalid hierarchy string return ""; } return string(hierarchy) + string(hierarchySeparator_) + to_string(childIndex); } inline static bool CheckIsDescendantHierarchy(string_view hierarchy, string_view hierarchyRoot) { // child node hierarchy must startswith parent node hierarchy return hierarchy.find(hierarchyRoot) == 0; } private: static constexpr auto hierarchySeparator_ = ","; }; static void SetWidgetBounds(Widget &widget, string_view boundsStr) { // set bounds int32_t val = -1; int32_t integers[4]; int32_t index = 0; bool negative = false; static constexpr int32_t FACTOR = 10; for (char ch : boundsStr) { if (ch == '-') { DCHECK(val == -1); // should be a start of a number negative = true; } else if (ch >= '0' && ch <= '9') { val = max(val, 0); // ensure accumulation val = val * FACTOR + (int32_t)(ch - '0'); } else if (val >= 0) { DCHECK(index < INDEX_FOUR); integers[index] = val * (negative ? -1 : 1); // after harvest, rest variables and increase ptrIdx index++; val = -1; negative = false; } } DCHECK(index == INDEX_FOUR); auto rect = Rect(integers[INDEX_ZERO], integers[INDEX_TWO], integers[INDEX_ONE], integers[INDEX_THREE]); widget.SetBounds(rect); } static void SetWidgetAttributes(Widget &widget, const map &attributes) { for (auto &item : attributes) { if (item.first == ATTR_NAMES[UiAttr::BOUNDS]) { SetWidgetBounds(widget, item.second); } else { widget.SetAttr(item.first, item.second); } } } void WidgetTree::DfsTraverse(WidgetVisitor &visitor) const { auto root = GetRootWidget(); if (root != nullptr) { DfsTraverseDescendants(visitor, *root); } } void WidgetTree::DfsTraverseFronts(WidgetVisitor &visitor, const Widget &pivot) const { DCHECK(widgetsConstructed_); DCHECK(CheckIsMyNode(pivot)); auto root = GetRootWidget(); if (root == nullptr) { return; } auto pivotHierarchy = pivot.GetHierarchy(); for (auto &hierarchy : widgetHierarchyIdDfsOrder_) { if (hierarchy == pivotHierarchy) { // hit the pivot, finish traversing break; } auto widget = widgetMap_.find(hierarchy); DCHECK(widget != widgetMap_.end()); visitor.Visit(widget->second); } } void WidgetTree::DfsTraverseRears(WidgetVisitor &visitor, const Widget &pivot) const { DCHECK(widgetsConstructed_); DCHECK(CheckIsMyNode(pivot)); auto root = GetRootWidget(); if (root == nullptr) { return; } auto pivotHierarchy = pivot.GetHierarchy(); bool traverseStarted = false; for (auto &hierarchy : widgetHierarchyIdDfsOrder_) { if (hierarchy == pivotHierarchy) { // skip self and start traverse from next one traverseStarted = true; continue; } if (!traverseStarted) { // skip front widgets continue; } auto widget = widgetMap_.find(hierarchy); DCHECK(widget != widgetMap_.end()); visitor.Visit(widget->second); } } void WidgetTree::DfsTraverseParents(WidgetVisitor &visitor, const Widget &pivot) const { DCHECK(widgetsConstructed_); DCHECK(CheckIsMyNode(pivot)); auto root = GetRootWidget(); if (root == nullptr) { return; } auto parent = this->GetParentWidget(pivot); while (true) { if (parent == nullptr) { break; } visitor.Visit(*parent); parent = this->GetParentWidget(*parent); } } void WidgetTree::DfsTraverseDescendants(WidgetVisitor &visitor, const Widget &root) const { DCHECK(widgetsConstructed_); DCHECK(CheckIsMyNode(root)); bool traverseStarted = false; auto rootHierarchy = root.GetHierarchy(); for (auto &hierarchy : widgetHierarchyIdDfsOrder_) { if (!WidgetHierarchyBuilder::CheckIsDescendantHierarchy(hierarchy, rootHierarchy)) { if (!traverseStarted) { continue; // root node not found yet, skip visiting current widget and go ahead } else { break; // descendant nodes are all visited, break } } traverseStarted = true; auto widget = widgetMap_.find(hierarchy); DCHECK(widget != widgetMap_.end()); visitor.Visit(widget->second); } } using NodeVisitor = function &&)>; static void DfsVisitNode(const json &root, NodeVisitor visitor, string_view hierarchy) { DCHECK(visitor != nullptr); auto attributesData = root["attributes"]; auto childrenData = root["children"]; map attributeDict; if (root.find("abilityName") != root.end()) { attributeDict["abilityName"] = root["abilityName"]; } if (root.find("bundleName") != root.end()) { attributeDict["bundleName"] = root["bundleName"]; } if (root.find("pagePath") != root.end()) { attributeDict["pagePath"] = root["pagePath"]; } for (auto &item : attributesData.items()) { attributeDict[item.key()] = item.value(); } visitor(hierarchy, move(attributeDict)); const size_t childCount = childrenData.size(); for (size_t idx = 0; idx < childCount; idx++) { auto &child = childrenData.at(idx); auto childHierarchy = WidgetHierarchyBuilder::Build(hierarchy, idx); DfsVisitNode(child, visitor, childHierarchy); } } void WidgetTree::EnsureParentVisible(const Widget &widget) { auto hierarchy = widget.GetHierarchy(); auto findParent = widgetMap_.find(WidgetHierarchyBuilder::GetParentWidgetHierarchy(hierarchy)); if (findParent == widgetMap_.end()) { return; } else if (!findParent->second.IsVisible()) { findParent->second.SetAttr(ATTR_NAMES[UiAttr::VISIBLE], "true"); EnsureParentVisible(findParent->second); } } static Rect GetEffectiveBounds(const Widget &widget, vector> &boundsClips) { auto result = Rect(0, 0, 0, 0); auto boundsClip = Rect(0, 0, 0, 0); if (boundsClips.empty()) { boundsClip = widget.GetBounds(); } else { while (widget.GetHierarchy().find(boundsClips.back().first) == std::string::npos) { boundsClips.pop_back(); if (boundsClips.empty()) { boundsClip = widget.GetBounds(); break; } } if (!boundsClips.empty()) { boundsClip = boundsClips.back().second; } } if (!RectAlgorithm::ComputeIntersection(widget.GetBounds(), boundsClip, result)) { result = Rect(0, 0, 0, 0); } return result; } void WidgetTree::ConstructFromDom(const nlohmann::json &dom, bool amendBounds) { DCHECK(!widgetsConstructed_); static const set containerTypes = {"List", "Grid", "WaterFlow", "GridCol", "GridRow", "Scroll", "Flex", "ListItemGroup", "Swiper", "DecorBar", "_Common_", "TabContent", "WindowScene"}; map> widgetDict; vector visitTrace; auto nodeVisitor = [&widgetDict, &visitTrace](string_view hierarchy, map &&attrs) { visitTrace.emplace_back(hierarchy); widgetDict.insert(make_pair(hierarchy, attrs)); }; DfsVisitNode(dom, nodeVisitor, ROOT_HIERARCHY); vector > boundsClips; for (auto &hierarchy : visitTrace) { auto findWidgetAttrs = widgetDict.find(hierarchy); DCHECK(findWidgetAttrs != widgetDict.end()); Widget widget(hierarchy); widget.SetHostTreeId(this->identifier_); SetWidgetAttributes(widget, findWidgetAttrs->second); const auto bounds = widget.GetBounds(); auto newBounds = Rect(0, 0, 0, 0); if (!amendBounds || hierarchy == ROOT_HIERARCHY) { newBounds = bounds; } else { newBounds = GetEffectiveBounds(widget, boundsClips); } if (!RectAlgorithm::CheckEqual(newBounds, bounds)) { widget.SetBounds(newBounds); LOG_D("Amend bounds %{public}s from %{public}s", widget.ToStr().c_str(), Rect2JsonStr(bounds).c_str()); } if (!amendBounds || (newBounds.GetWidth() > 0 && newBounds.GetHeight() > 0)) { widget.SetAttr(ATTR_NAMES[UiAttr::VISIBLE], "true"); } if (widget.IsVisible()) { EnsureParentVisible(widget); } auto type = widget.GetAttr(ATTR_NAMES[UiAttr::TYPE], ""); if (containerTypes.find(type) != containerTypes.end()) { boundsClips.push_back(make_pair(widget.GetHierarchy(), widget.GetBounds())); } widgetMap_.insert(make_pair(hierarchy, move(widget))); widgetHierarchyIdDfsOrder_.emplace_back(hierarchy); } widgetsConstructed_ = true; } static void DfsMarshalWidget(const WidgetTree& tree, const Widget& root, nlohmann::json& dom, const std::map& widgetChildCountMap) { auto attributesData = json(); // "< UiAttr::HIERARCHY" : do not expose inner used attributes for (auto index = 0; index < UiAttr::HIERARCHY; index++) { const auto attr = ATTR_NAMES[index].data(); attributesData[attr] = root.GetAttr(attr, ""); } if (root.HasAttr("abilityName")) { attributesData["abilityName"] = root.GetAttr("abilityName", ""); } if (root.HasAttr("bundleName")) { attributesData["bundleName"] = root.GetAttr("bundleName", ""); } if (root.HasAttr("pagePath")) { attributesData["pagePath"] = root.GetAttr("pagePath", ""); } stringstream stream; auto rect = root.GetBounds(); stream << "[" << rect.left_ << "," << rect.top_ << "]" << "[" << rect.right_ << "," << rect.bottom_ << "]"; attributesData[ATTR_NAMES[UiAttr::BOUNDS].data()] = stream.str(); auto childrenData = json::array(); uint32_t childIndex = 0; uint32_t childCount = 0; uint32_t visitCount = 0; auto hierarchy = root.GetHierarchy(); if (widgetChildCountMap.find(hierarchy) != widgetChildCountMap.end()) { childCount = widgetChildCountMap.find(hierarchy)->second; } while (visitCount < childCount) { auto child = tree.GetChildWidget(root, childIndex); childIndex++; if (child == nullptr) { continue; } if (!child->IsVisible()) { visitCount++; continue; } auto childData = json(); DfsMarshalWidget(tree, *child, childData, widgetChildCountMap); childrenData.emplace_back(childData); visitCount++; } dom["attributes"] = attributesData; dom["children"] = childrenData; } void WidgetTree::MarshalIntoDom(nlohmann::json& dom) const { DCHECK(widgetsConstructed_); auto root = GetRootWidget(); std::map widgetChildCountMap; for (auto &hierarchy : widgetHierarchyIdDfsOrder_) { if (hierarchy == ROOT_HIERARCHY) { continue; } auto parentHierarchy = WidgetHierarchyBuilder::GetParentWidgetHierarchy(hierarchy); if (widgetChildCountMap.find(parentHierarchy) == widgetChildCountMap.end()) { widgetChildCountMap[parentHierarchy] = 1; } else { widgetChildCountMap[parentHierarchy] = widgetChildCountMap[parentHierarchy] + 1; } } if (root != nullptr) { DfsMarshalWidget(*this, *root, dom, widgetChildCountMap); } } const Widget *WidgetTree::GetRootWidget() const { return GetWidgetByHierarchy(ROOT_HIERARCHY); } const Widget *WidgetTree::GetParentWidget(const Widget &widget) const { DCHECK(CheckIsMyNode(widget)); if (widget.GetHierarchy() == ROOT_HIERARCHY) { return nullptr; } const auto parentHierarchy = WidgetHierarchyBuilder::GetParentWidgetHierarchy(widget.GetHierarchy()); return GetWidgetByHierarchy(parentHierarchy); } const Widget *WidgetTree::GetChildWidget(const Widget &widget, uint32_t index) const { DCHECK(CheckIsMyNode(widget)); if (index < 0) { return nullptr; } auto childHierarchy = WidgetHierarchyBuilder::GetChildHierarchy(widget.GetHierarchy(), index); return GetWidgetByHierarchy(childHierarchy); } const Widget *WidgetTree::GetWidgetByHierarchy(string_view hierarchy) const { auto findWidget = widgetMap_.find(string(hierarchy)); if (findWidget != widgetMap_.end()) { return &(findWidget->second); } return nullptr; } bool WidgetTree::IsRootWidgetHierarchy(string_view hierarchy) { return ROOT_HIERARCHY == hierarchy; } string WidgetTree::GenerateTreeId() { static uint32_t counter = 0; auto id = string("WidgetTree@") + to_string(counter); counter++; return id; } inline bool WidgetTree::CheckIsMyNode(const Widget &widget) const { return this->identifier_ == widget.GetHostTreeId(); } /** WidgetVisitor used to visit and merge widgets of subtrees into dest root tree.*/ class MergerVisitor : public WidgetVisitor { public: // handler function to perform merging widget, arg1: the revised bounds using MergerFunction = function; explicit MergerVisitor(MergerFunction collector) : collector_(collector) {}; ~MergerVisitor() {} void PrepareToVisitSubTree(const WidgetTree &tree); void EndVisitingSubTree(); void Visit(const Widget &widget) override; Rect GetMergedBounds() const { return mergedBounds_; } private: MergerFunction collector_; // the overlays of widget vector overlays_; // the node hierarchy of first invisble parent string maxInvisibleParent_ = "NA"; // the node hierarchy of first fully-visible parent string maxFullyParent_ = "NA"; // the merged bounds Rect mergedBounds_ = {0, 0, 0, 0}; // bounds of current visiting tree Rect visitingTreeBounds_ = {0, 0, 0, 0}; }; void MergerVisitor::PrepareToVisitSubTree(const WidgetTree &tree) { auto root = tree.GetRootWidget(); DCHECK(root != nullptr); // collect max bounds auto rootBounds = root->GetBounds(); mergedBounds_.left_ = min(mergedBounds_.left_, rootBounds.left_); mergedBounds_.top_ = min(mergedBounds_.top_, rootBounds.top_); mergedBounds_.right_ = max(mergedBounds_.right_, rootBounds.right_); mergedBounds_.bottom_ = max(mergedBounds_.bottom_, rootBounds.bottom_); // update visiting tree bounds visitingTreeBounds_ = rootBounds; // reset intermediate data maxInvisibleParent_ = "NA"; maxFullyParent_ = "NA"; } void MergerVisitor::EndVisitingSubTree() { // add overlays for later visited subtrees overlays_.push_back(visitingTreeBounds_); } void MergerVisitor::Visit(const Widget &widget) { if (collector_ == nullptr) { return; } const auto hierarchy = widget.GetHierarchy(); const auto inInVisible = WidgetHierarchyBuilder::CheckIsDescendantHierarchy(hierarchy, maxInvisibleParent_); const auto inFully = WidgetHierarchyBuilder::CheckIsDescendantHierarchy(hierarchy, maxFullyParent_); if (inInVisible) { // parent invisible, skip return; } const auto bounds = widget.GetBounds(); bool visible = true; auto visibleRegion = bounds; if (!inFully) { // parent not full-visible, need compute visible region visible = RectAlgorithm::ComputeMaxVisibleRegion(bounds, overlays_, visibleRegion); } if (!visible) { maxInvisibleParent_ = hierarchy; // update maxInvisibleParent return; } if (!inFully && RectAlgorithm::CheckEqual(bounds, visibleRegion)) { maxFullyParent_ = hierarchy; // update maxFullParent } // call collector with widget and revised bounds collector_(widget, visibleRegion); } void WidgetTree::MergeTrees(const vector> &from, WidgetTree &to, vector &mergedOrders) { if (from.empty()) { return; } size_t subtreeIndex = 0; size_t hierarchyIndex = 0; to.widgetsConstructed_ = true; auto virtualRoot = Widget(ROOT_HIERARCHY); virtualRoot.SetAttr(ATTR_NAMES[UiAttr::VISIBLE], "true"); virtualRoot.SetHostTreeId(to.identifier_); // insert virtual root node into tree to.widgetHierarchyIdDfsOrder_.emplace_back(ROOT_HIERARCHY); to.widgetMap_.insert(make_pair(ROOT_HIERARCHY, move(virtualRoot))); auto &vitualRootWidget = to.widgetMap_.begin()->second; // amend widget hierarchy with prefix to make it the descendant of the virtualRoot string hierarchyPrefix = ""; constexpr auto offset = string_view(ROOT_HIERARCHY).length(); // collect widget with revised hierarchy and bounds, merge it to dest tree auto merger = [&hierarchyPrefix, &tree = to, &subtreeIndex, &hierarchyIndex, &mergedOrders] (const Widget &widget, const Rect &bounds) { auto newHierarchy = string(hierarchyPrefix) + widget.GetHierarchy().substr(offset); auto newWidget = widget.Clone(tree.identifier_, newHierarchy); newWidget->SetBounds(bounds); if (widget.GetHierarchy() == ROOT_HIERARCHY) { mergedOrders.push_back(subtreeIndex); hierarchyIndex++; } tree.widgetMap_.insert(make_pair(newHierarchy, move(*newWidget))); tree.widgetHierarchyIdDfsOrder_.emplace_back(newHierarchy); }; auto visitor = MergerVisitor(merger); for (auto &tree : from) { DCHECK(tree != nullptr); // update hierarchy prefix hierarchyPrefix = string(ROOT_HIERARCHY) + "," + to_string(hierarchyIndex); // visit tree and forward visible widgets to collector visitor.PrepareToVisitSubTree(*tree); tree->DfsTraverse(visitor); visitor.EndVisitingSubTree(); subtreeIndex++; } // amend bounds of the virtual root vitualRootWidget.SetBounds(visitor.GetMergedBounds()); } void TreeSnapshotTaker::Visit(const Widget &widget) { auto type = widget.GetAttr(ATTR_NAMES[UiAttr::TYPE], "") + "/"; auto value = widget.GetAttr(ATTR_NAMES[UiAttr::TEXT], "") + "/"; auto hashcode = widget.GetAttr(ATTR_NAMES[UiAttr::HASHCODE], "") + ";"; if (value != "/") { allNodes_.push_back(type + value + hashcode); if ((widget.GetAttr(ATTR_NAMES[UiAttr::VISIBLE], "")) == "true") { displayNodes_.push_back(type + value + hashcode); } } } } // namespace OHOS::uitest