1 /*
2  * Copyright (C) 2024 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 #include "core/common/ai/data_detector_adapter.h"
17 
18 #include "adapter/ohos/entrance/ace_container.h"
19 #include "core/common/ai/data_detector_mgr.h"
20 #include "core/pipeline_ng/pipeline_context.h"
21 
22 namespace OHOS::Ace {
23 
24 constexpr int32_t AI_TEXT_MAX_LENGTH = 500;
25 constexpr int32_t AI_TEXT_GAP = 100;
26 constexpr int32_t AI_DELAY_TIME = 100;
27 constexpr uint32_t SECONDS_TO_MILLISECONDS = 1000;
28 
29 const std::unordered_map<TextDataDetectType, std::string> TEXT_DETECT_MAP = {
30     { TextDataDetectType::PHONE_NUMBER, "phoneNum" }, { TextDataDetectType::URL, "url" },
31     { TextDataDetectType::EMAIL, "email" }, { TextDataDetectType::ADDRESS, "location" },
32     { TextDataDetectType::DATE_TIME, "datetime" }
33 };
34 const std::unordered_map<std::string, TextDataDetectType> TEXT_DETECT_MAP_REVERSE = {
35     { "phoneNum", TextDataDetectType::PHONE_NUMBER }, { "url", TextDataDetectType::URL },
36     { "email", TextDataDetectType::EMAIL }, { "location", TextDataDetectType::ADDRESS },
37     { "datetime", TextDataDetectType::DATE_TIME }
38 };
39 
GetAIEntityMenu()40 void DataDetectorAdapter::GetAIEntityMenu()
41 {
42     auto context = PipelineContext::GetCurrentContextSafely();
43     CHECK_NULL_VOID(context);
44     auto uiTaskExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::UI);
45     uiTaskExecutor.PostTask(
46         [weak = AceType::WeakClaim(this), instanceId = context->GetInstanceId()] {
47             ContainerScope scope(instanceId);
48             auto dataDetectorAdapter = weak.Upgrade();
49             CHECK_NULL_VOID(dataDetectorAdapter);
50             TAG_LOGI(AceLogTag::ACE_TEXT, "Get AI entity menu from ai_engine");
51             DataDetectorMgr::GetInstance().GetAIEntityMenu(dataDetectorAdapter->textDetectResult_);
52         },
53         "ArkUITextInitDataDetect");
54 }
55 
ShowAIEntityMenu(const AISpan& aiSpan, const NG::RectF& aiRect, const RefPtr<NG::FrameNode>& targetNode, bool isShowCopy, bool isShowSelectText)56 bool DataDetectorAdapter::ShowAIEntityMenu(const AISpan& aiSpan, const NG::RectF& aiRect,
57     const RefPtr<NG::FrameNode>& targetNode, bool isShowCopy, bool isShowSelectText)
58 {
59     if (textDetectResult_.menuOptionAndAction.empty()) {
60         TAG_LOGW(AceLogTag::ACE_TEXT, "menu option is empty, please try again");
61         GetAIEntityMenu();
62         return false;
63     }
64 
65     mainContainerId_ = Container::CurrentId();
66     std::vector<std::pair<std::string, std::function<void()>>> menuOptions;
67     auto menuOptionAndAction = textDetectResult_.menuOptionAndAction[TEXT_DETECT_MAP.at(aiSpan.type)];
68     if (menuOptionAndAction.empty()) {
69         return false;
70     }
71     if (!isShowSelectText) {
72         // delete the last option: selectText.
73         menuOptionAndAction.pop_back();
74         if (!isShowCopy) {
75         // delete the last option: copy.
76             menuOptionAndAction.pop_back();
77         }
78     }
79 
80     for (auto menuOption : menuOptionAndAction) {
81         std::function<void()> onClickEvent = [aiSpan, menuOption, weak = AceType::WeakClaim(this),
82                                                  targetNodeWeak = AceType::WeakClaim(AceType::RawPtr(targetNode))]() {
83             auto dataDetectorAdapter = weak.Upgrade();
84             CHECK_NULL_VOID(dataDetectorAdapter);
85             auto targetNode = targetNodeWeak.Upgrade();
86             CHECK_NULL_VOID(targetNode);
87             dataDetectorAdapter->OnClickAIMenuOption(aiSpan, menuOption, targetNode);
88         };
89         menuOptions.push_back(std::make_pair(menuOption.first, onClickEvent));
90     }
91     auto pipeline = NG::PipelineContext::GetCurrentContextSafely();
92     CHECK_NULL_RETURN(pipeline, false);
93     auto overlayManager = pipeline->GetOverlayManager();
94     CHECK_NULL_RETURN(overlayManager, false);
95     return overlayManager->ShowAIEntityMenu(menuOptions, aiRect, targetNode);
96 }
97 
OnClickAIMenuOption(const AISpan& aiSpan, const std::pair<std::string, FuncVariant>& menuOption, const RefPtr<NG::FrameNode>& targetNode)98 void DataDetectorAdapter::OnClickAIMenuOption(const AISpan& aiSpan,
99     const std::pair<std::string, FuncVariant>& menuOption, const RefPtr<NG::FrameNode>& targetNode)
100 {
101     TAG_LOGI(AceLogTag::ACE_TEXT, "Click AI menu option: %{public}s", menuOption.first.c_str());
102     auto pipeline = NG::PipelineContext::GetCurrentContextSafely();
103     CHECK_NULL_VOID(pipeline);
104     auto overlayManager = pipeline->GetOverlayManager();
105     CHECK_NULL_VOID(overlayManager);
106     if (targetNode) {
107         overlayManager->CloseAIEntityMenu(targetNode->GetId());
108     }
109     Container::UpdateCurrent(mainContainerId_);
110 
111     auto runtimeContext = Platform::AceContainer::GetRuntimeContext(pipeline->GetInstanceId());
112     CHECK_NULL_VOID(runtimeContext);
113     auto token = runtimeContext->GetToken();
114     auto bundleName = runtimeContext->GetBundleName();
115 
116     hasClickedMenuOption_ = true;
117     if (onClickMenu_ && std::holds_alternative<std::function<std::string()>>(menuOption.second)) {
118         onClickMenu_(std::get<std::function<std::string()>>(menuOption.second)());
119     } else if (std::holds_alternative<std::function<void(sptr<IRemoteObject>, std::string)>>(menuOption.second)) {
120         std::get<std::function<void(sptr<IRemoteObject>, std::string)>>(menuOption.second)(token, aiSpan.content);
121     } else if (std::holds_alternative<std::function<void(int32_t, std::string)>>(menuOption.second)) {
122         std::get<std::function<void(int32_t, std::string)>>(menuOption.second)(mainContainerId_, aiSpan.content);
123     } else if (std::holds_alternative<std::function<void(int32_t, std::string, std::string, int32_t, std::string)>>(
124                    menuOption.second)) {
125         std::get<std::function<void(int32_t, std::string, std::string, int32_t, std::string)>>(menuOption.second)(
126             mainContainerId_, textForAI_, bundleName, aiSpan.start, aiSpan.content);
127     } else {
128         TAG_LOGW(AceLogTag::ACE_TEXT, "No matching menu option");
129     }
130     hasClickedMenuOption_ = false;
131 }
132 
ResponseBestMatchItem(const AISpan& aiSpan)133 void DataDetectorAdapter::ResponseBestMatchItem(const AISpan& aiSpan)
134 {
135     if (textDetectResult_.menuOptionAndAction.empty()) {
136         TAG_LOGW(AceLogTag::ACE_TEXT, "menu option is empty, please try again");
137         GetAIEntityMenu();
138         return;
139     }
140     auto menuOptions = textDetectResult_.menuOptionAndAction[TEXT_DETECT_MAP.at(aiSpan.type)];
141     if (menuOptions.empty()) {
142         TAG_LOGW(AceLogTag::ACE_TEXT, "menu option is empty");
143         return;
144     }
145     OnClickAIMenuOption(aiSpan, menuOptions[0]);
146 }
147 
SetTextDetectTypes(const std::string& types)148 void DataDetectorAdapter::SetTextDetectTypes(const std::string& types)
149 {
150     textDetectTypes_ = types;
151 
152     std::set<std::string> newTypesSet;
153     std::istringstream iss(types);
154     std::string type;
155     while (std::getline(iss, type, ',')) {
156         newTypesSet.insert(type);
157     }
158     if (newTypesSet != textDetectTypesSet_) {
159         textDetectTypesSet_ = newTypesSet;
160         typeChanged_ = true;
161         aiDetectInitialized_ = false;
162         auto host = GetHost();
163         CHECK_NULL_VOID(host);
164         host->MarkDirtyNode(NG::PROPERTY_UPDATE_MEASURE);
165     }
166 }
167 
ParseOriText(const std::unique_ptr<JsonValue>& entityJson, std::string& text)168 bool DataDetectorAdapter::ParseOriText(const std::unique_ptr<JsonValue>& entityJson, std::string& text)
169 {
170     TAG_LOGI(AceLogTag::ACE_TEXT, "Parse origin text entry");
171     auto runtimeContext = Platform::AceContainer::GetRuntimeContext(Container::CurrentId());
172     CHECK_NULL_RETURN(runtimeContext, false);
173     if (runtimeContext->GetBundleName() != entityJson->GetString("bundleName")) {
174         TAG_LOGW(AceLogTag::ACE_TEXT,
175             "Wrong bundleName, the context bundleName is: %{public}s, but your bundleName is: %{public}s",
176             runtimeContext->GetBundleName().c_str(), entityJson->GetString("bundleName").c_str());
177         return false;
178     }
179     auto aiSpanArray = entityJson->GetValue("entity");
180     if (aiSpanArray->IsNull() || !aiSpanArray->IsArray()) {
181         TAG_LOGW(AceLogTag::ACE_TEXT, "Wrong AI entity");
182         return false;
183     }
184 
185     aiSpanMap_.clear();
186     aiSpanRects_.clear();
187     detectTexts_.clear();
188     AISpan aiSpan;
189     for (int32_t i = 0; i < aiSpanArray->GetArraySize(); ++i) {
190         auto item = aiSpanArray->GetArrayItem(i);
191         aiSpan.content = item->GetString("entityContent");
192         aiSpan.type = TEXT_DETECT_MAP_REVERSE.at(item->GetString("entityType"));
193         aiSpan.start = item->GetInt("start");
194         aiSpan.end = item->GetInt("end");
195         aiSpanMap_[aiSpan.start] = aiSpan;
196     }
197     aiDetectInitialized_ = true;
198     text = entityJson->GetString("content");
199     textForAI_ = text;
200     lastTextForAI_ = textForAI_;
201     if (textDetectResult_.menuOptionAndAction.empty()) {
202         GetAIEntityMenu();
203     }
204 
205     TAG_LOGI(AceLogTag::ACE_TEXT, "Parse origin text successful");
206     return true;
207 }
208 
InitTextDetect(int32_t startPos, std::string detectText)209 void DataDetectorAdapter::InitTextDetect(int32_t startPos, std::string detectText)
210 {
211     TextDataDetectInfo info;
212     info.text = detectText;
213     info.module = textDetectTypes_;
214 
215     auto context = PipelineContext::GetCurrentContextSafely();
216     CHECK_NULL_VOID(context);
217     int32_t instanceID = context->GetInstanceId();
218     auto textFunc = [weak = WeakClaim(this), instanceID, startPos, info](const TextDataDetectResult result) {
219         ContainerScope scope(instanceID);
220         auto context = PipelineContext::GetCurrentContextSafely();
221         CHECK_NULL_VOID(context);
222         auto uiTaskExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::UI);
223         uiTaskExecutor.PostTask(
224             [result, weak, instanceID, startPos, info] {
225                 ContainerScope scope(instanceID);
226                 auto dataDetectorAdapter = weak.Upgrade();
227                 CHECK_NULL_VOID(dataDetectorAdapter);
228                 if (info.module != dataDetectorAdapter->textDetectTypes_) {
229                     return;
230                 }
231                 dataDetectorAdapter->ParseAIResult(result, startPos);
232                 auto host = dataDetectorAdapter->GetHost();
233                 CHECK_NULL_VOID(host);
234                 host->MarkDirtyNode(NG::PROPERTY_UPDATE_MEASURE);
235             },
236             "ArkUITextParseAIResult");
237     };
238 
239     auto uiTaskExecutor = SingleTaskExecutor::Make(context->GetTaskExecutor(), TaskExecutor::TaskType::BACKGROUND);
240     uiTaskExecutor.PostTask(
241         [info, textFunc] {
242             TAG_LOGI(AceLogTag::ACE_TEXT, "Start entity detect using AI");
243             DataDetectorMgr::GetInstance().DataDetect(info, textFunc);
244         },
245         "ArkUITextInitDataDetect");
246 }
247 
ParseAIResult(const TextDataDetectResult& result, int32_t startPos)248 void DataDetectorAdapter::ParseAIResult(const TextDataDetectResult& result, int32_t startPos)
249 {
250     auto entityJson = JsonUtil::ParseJsonString(result.entity);
251     CHECK_NULL_VOID(entityJson);
252     for (const auto& type : TEXT_DETECT_MAP) {
253         auto jsonValue = entityJson->GetValue(type.second);
254         ParseAIJson(jsonValue, type.first, startPos);
255     }
256 
257     if (startPos + AI_TEXT_MAX_LENGTH >= static_cast<int32_t>(StringUtils::ToWstring(textForAI_).length())) {
258         aiDetectInitialized_ = true;
259         auto entityJsonArray = JsonUtil::CreateArray(true);
260         // process with overlapping entities, leaving only the earlier ones
261         int32_t preEnd = 0;
262         auto aiSpanIterator = aiSpanMap_.begin();
263         while (aiSpanIterator != aiSpanMap_.end()) {
264             auto aiSpan = aiSpanIterator->second;
265             if (aiSpan.start < preEnd) {
266                 aiSpanIterator = aiSpanMap_.erase(aiSpanIterator);
267             } else {
268                 preEnd = aiSpan.end;
269                 ++aiSpanIterator;
270                 auto aiSpanJson = JsonUtil::Create(true);
271                 aiSpanJson->Put("start", aiSpan.start);
272                 aiSpanJson->Put("end", aiSpan.end);
273                 aiSpanJson->Put("entityContent", aiSpan.content.c_str());
274                 aiSpanJson->Put("entityType", TEXT_DETECT_MAP.at(aiSpan.type).c_str());
275                 entityJsonArray->Put(aiSpanJson);
276             }
277         }
278         auto resultJson = JsonUtil::Create(true);
279         resultJson->Put("entity", entityJsonArray);
280         resultJson->Put("code", result.code);
281         SetTextDetectResult(result);
282         FireOnResult(resultJson->ToString());
283     }
284 }
285 
ParseAIJson( const std::unique_ptr<JsonValue>& jsonValue, TextDataDetectType type, int32_t startPos)286 void DataDetectorAdapter::ParseAIJson(
287     const std::unique_ptr<JsonValue>& jsonValue, TextDataDetectType type, int32_t startPos)
288 {
289     if (!jsonValue || !jsonValue->IsArray()) {
290         TAG_LOGW(AceLogTag::ACE_TEXT, "Wrong AI result");
291         return;
292     }
293 
294     for (int32_t i = 0; i < jsonValue->GetArraySize(); ++i) {
295         auto item = jsonValue->GetArrayItem(i);
296         auto charOffset = item->GetInt("charOffset");
297         auto oriText = item->GetString("oriText");
298         auto wTextForAI = StringUtils::ToWstring(textForAI_);
299         auto wOriText = StringUtils::ToWstring(oriText);
300         int32_t end = startPos + charOffset + static_cast<int32_t>(wOriText.length());
301         if (charOffset < 0 || startPos + charOffset >= static_cast<int32_t>(wTextForAI.length()) ||
302             end >= startPos + AI_TEXT_MAX_LENGTH || oriText.empty()) {
303             TAG_LOGW(AceLogTag::ACE_TEXT, "The result of AI is wrong");
304             continue;
305         }
306         if (oriText !=
307             StringUtils::ToString(wTextForAI.substr(startPos + charOffset, static_cast<int32_t>(wOriText.length())))) {
308             TAG_LOGW(AceLogTag::ACE_TEXT, "The charOffset is wrong");
309             continue;
310         }
311         int32_t start = startPos + charOffset;
312         auto iter = aiSpanMap_.find(start);
313         if (iter != aiSpanMap_.end() && iter->second.content.length() >= oriText.length()) {
314             // both entities start at the same position, leaving the longer one
315             continue;
316         }
317 
318         TimeStamp currentDetectorTimeStamp = std::chrono::high_resolution_clock::now();
319         std::chrono::duration<float, std::ratio<1, SECONDS_TO_MILLISECONDS>> costTime =
320             currentDetectorTimeStamp - startDetectorTimeStamp_;
321         item->Put("costTime", costTime.count());
322         item->Put("resultCode", textDetectResult_.code);
323         entityJson_[start] = item->ToString();
324         TAG_LOGI(AceLogTag::ACE_TEXT, "The json of the entity is: %{private}s", entityJson_[start].c_str());
325 
326         AISpan aiSpan;
327         aiSpan.start = start;
328         aiSpan.end = end;
329         aiSpan.content = oriText;
330         aiSpan.type = type;
331         aiSpanMap_[aiSpan.start] = aiSpan;
332     }
333 }
334 
GetDetectDelayTask(const std::map<int32_t, AISpan>& aiSpanMap)335 std::function<void()> DataDetectorAdapter::GetDetectDelayTask(const std::map<int32_t, AISpan>& aiSpanMap)
336 {
337     return [aiSpanMap, weak = WeakClaim(this)]() {
338         auto dataDetectorAdapter = weak.Upgrade();
339         CHECK_NULL_VOID(dataDetectorAdapter);
340         if (dataDetectorAdapter->textForAI_.empty()) {
341             return;
342         }
343 
344         size_t detectTextIdx = 0;
345         auto aiSpanMapIt = aiSpanMap.begin();
346         int32_t startPos = 0;
347         bool hasSame = false;
348         auto wTextForAI = StringUtils::ToWstring(dataDetectorAdapter->textForAI_);
349         auto wTextForAILength = static_cast<int32_t>(wTextForAI.length());
350         do {
351             std::string detectText = StringUtils::ToString(
352                 wTextForAI.substr(startPos, std::min(AI_TEXT_MAX_LENGTH, wTextForAILength - startPos)));
353             bool isSameDetectText = detectTextIdx < dataDetectorAdapter->detectTexts_.size() &&
354                                     detectText == dataDetectorAdapter->detectTexts_[detectTextIdx];
355             while (!aiSpanMap.empty() && aiSpanMapIt != aiSpanMap.end() && aiSpanMapIt->first >= 0 &&
356                    aiSpanMapIt->first < std::min(wTextForAILength, startPos + AI_TEXT_MAX_LENGTH - AI_TEXT_GAP)) {
357                 auto aiContent = aiSpanMapIt->second.content;
358                 auto wAIContent = StringUtils::ToWstring(aiContent);
359                 if (isSameDetectText || aiContent == StringUtils::ToString(wTextForAI.substr(aiSpanMapIt->first,
360                     std::min(static_cast<int32_t>(wAIContent.length()), wTextForAILength - aiSpanMapIt->first)))) {
361                     dataDetectorAdapter->aiSpanMap_[aiSpanMapIt->first] = aiSpanMapIt->second;
362                     hasSame = true;
363                 }
364                 ++aiSpanMapIt;
365             }
366             if (!isSameDetectText) {
367                 dataDetectorAdapter->InitTextDetect(startPos, detectText);
368                 if (detectTextIdx < dataDetectorAdapter->detectTexts_.size()) {
369                     dataDetectorAdapter->detectTexts_[detectTextIdx] = detectText;
370                 } else {
371                     dataDetectorAdapter->detectTexts_.emplace_back(detectText);
372                 }
373             }
374             ++detectTextIdx;
375             startPos += AI_TEXT_MAX_LENGTH - AI_TEXT_GAP;
376         } while (startPos + AI_TEXT_GAP < wTextForAILength);
377         if (hasSame) {
378             auto host = dataDetectorAdapter->GetHost();
379             CHECK_NULL_VOID(host);
380             host->MarkDirtyNode(NG::PROPERTY_UPDATE_MEASURE);
381         }
382     };
383 }
384 
StartAITask()385 void DataDetectorAdapter::StartAITask()
386 {
387     if (textForAI_.empty() || (!typeChanged_ && lastTextForAI_ == textForAI_)) {
388         auto host = GetHost();
389         CHECK_NULL_VOID(host);
390         host->MarkDirtyNode(NG::PROPERTY_UPDATE_MEASURE);
391         return;
392     }
393     std::map<int32_t, AISpan> aiSpanMapCopy;
394     if (!typeChanged_) {
395         aiSpanMapCopy = aiSpanMap_;
396     } else {
397         detectTexts_.clear();
398     }
399     aiSpanMap_.clear();
400     typeChanged_ = false;
401     lastTextForAI_ = textForAI_;
402     startDetectorTimeStamp_ = std::chrono::high_resolution_clock::now();
403     auto context = PipelineContext::GetCurrentContextSafely();
404     CHECK_NULL_VOID(context);
405     auto taskExecutor = context->GetTaskExecutor();
406     CHECK_NULL_VOID(taskExecutor);
407     aiDetectDelayTask_.Cancel();
408     aiDetectDelayTask_.Reset(GetDetectDelayTask(aiSpanMapCopy));
409     taskExecutor->PostDelayedTask(
410         aiDetectDelayTask_, TaskExecutor::TaskType::UI, AI_DELAY_TIME, "ArkUITextStartAIDetect");
411 }
412 } // namespace OHOS::Ace
413